File.php 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543
  1. <?php
  2. if (class_exists('ParagonIE_Sodium_File', false)) {
  3. return;
  4. }
  5. /**
  6. * Class ParagonIE_Sodium_File
  7. */
  8. class ParagonIE_Sodium_File extends ParagonIE_Sodium_Core_Util
  9. {
  10. /* PHP's default buffer size is 8192 for fread()/fwrite(). */
  11. const BUFFER_SIZE = 8192;
  12. /**
  13. * Box a file (rather than a string). Uses less memory than
  14. * ParagonIE_Sodium_Compat::crypto_box(), but produces
  15. * the same result.
  16. *
  17. * @param string $inputFile Absolute path to a file on the filesystem
  18. * @param string $outputFile Absolute path to a file on the filesystem
  19. * @param string $nonce Number to be used only once
  20. * @param string $keyPair ECDH secret key and ECDH public key concatenated
  21. *
  22. * @return bool
  23. * @throws SodiumException
  24. * @throws TypeError
  25. */
  26. public static function box($inputFile, $outputFile, $nonce, $keyPair)
  27. {
  28. /* Type checks: */
  29. if (!is_string($inputFile)) {
  30. throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
  31. }
  32. if (!is_string($outputFile)) {
  33. throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
  34. }
  35. if (!is_string($nonce)) {
  36. throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
  37. }
  38. /* Input validation: */
  39. if (!is_string($keyPair)) {
  40. throw new TypeError('Argument 4 must be a string, ' . gettype($keyPair) . ' given.');
  41. }
  42. if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_NONCEBYTES) {
  43. throw new TypeError('Argument 3 must be CRYPTO_BOX_NONCEBYTES bytes');
  44. }
  45. if (self::strlen($keyPair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) {
  46. throw new TypeError('Argument 4 must be CRYPTO_BOX_KEYPAIRBYTES bytes');
  47. }
  48. /** @var int $size */
  49. $size = filesize($inputFile);
  50. if (!is_int($size)) {
  51. throw new SodiumException('Could not obtain the file size');
  52. }
  53. /** @var resource $ifp */
  54. $ifp = fopen($inputFile, 'rb');
  55. if (!is_resource($ifp)) {
  56. throw new SodiumException('Could not open input file for reading');
  57. }
  58. /** @var resource $ofp */
  59. $ofp = fopen($outputFile, 'wb');
  60. if (!is_resource($ofp)) {
  61. fclose($ifp);
  62. throw new SodiumException('Could not open output file for writing');
  63. }
  64. $res = self::box_encrypt($ifp, $ofp, $size, $nonce, $keyPair);
  65. fclose($ifp);
  66. fclose($ofp);
  67. return $res;
  68. }
  69. /**
  70. * Open a boxed file (rather than a string). Uses less memory than
  71. * ParagonIE_Sodium_Compat::crypto_box_open(), but produces
  72. * the same result.
  73. *
  74. * Warning: Does not protect against TOCTOU attacks. You should
  75. * just load the file into memory and use crypto_box_open() if
  76. * you are worried about those.
  77. *
  78. * @param string $inputFile
  79. * @param string $outputFile
  80. * @param string $nonce
  81. * @param string $keypair
  82. * @return bool
  83. * @throws SodiumException
  84. * @throws TypeError
  85. */
  86. public static function box_open($inputFile, $outputFile, $nonce, $keypair)
  87. {
  88. /* Type checks: */
  89. if (!is_string($inputFile)) {
  90. throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
  91. }
  92. if (!is_string($outputFile)) {
  93. throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
  94. }
  95. if (!is_string($nonce)) {
  96. throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
  97. }
  98. if (!is_string($keypair)) {
  99. throw new TypeError('Argument 4 must be a string, ' . gettype($keypair) . ' given.');
  100. }
  101. /* Input validation: */
  102. if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_NONCEBYTES) {
  103. throw new TypeError('Argument 4 must be CRYPTO_BOX_NONCEBYTES bytes');
  104. }
  105. if (self::strlen($keypair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) {
  106. throw new TypeError('Argument 4 must be CRYPTO_BOX_KEYPAIRBYTES bytes');
  107. }
  108. /** @var int $size */
  109. $size = filesize($inputFile);
  110. if (!is_int($size)) {
  111. throw new SodiumException('Could not obtain the file size');
  112. }
  113. /** @var resource $ifp */
  114. $ifp = fopen($inputFile, 'rb');
  115. if (!is_resource($ifp)) {
  116. throw new SodiumException('Could not open input file for reading');
  117. }
  118. /** @var resource $ofp */
  119. $ofp = fopen($outputFile, 'wb');
  120. if (!is_resource($ofp)) {
  121. fclose($ifp);
  122. throw new SodiumException('Could not open output file for writing');
  123. }
  124. $res = self::box_decrypt($ifp, $ofp, $size, $nonce, $keypair);
  125. fclose($ifp);
  126. fclose($ofp);
  127. try {
  128. ParagonIE_Sodium_Compat::memzero($nonce);
  129. ParagonIE_Sodium_Compat::memzero($ephKeypair);
  130. } catch (SodiumException $ex) {
  131. unset($ephKeypair);
  132. }
  133. return $res;
  134. }
  135. /**
  136. * Seal a file (rather than a string). Uses less memory than
  137. * ParagonIE_Sodium_Compat::crypto_box_seal(), but produces
  138. * the same result.
  139. *
  140. * @param string $inputFile Absolute path to a file on the filesystem
  141. * @param string $outputFile Absolute path to a file on the filesystem
  142. * @param string $publicKey ECDH public key
  143. *
  144. * @return bool
  145. * @throws SodiumException
  146. * @throws TypeError
  147. */
  148. public static function box_seal($inputFile, $outputFile, $publicKey)
  149. {
  150. /* Type checks: */
  151. if (!is_string($inputFile)) {
  152. throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
  153. }
  154. if (!is_string($outputFile)) {
  155. throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
  156. }
  157. if (!is_string($publicKey)) {
  158. throw new TypeError('Argument 3 must be a string, ' . gettype($publicKey) . ' given.');
  159. }
  160. /* Input validation: */
  161. if (self::strlen($publicKey) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) {
  162. throw new TypeError('Argument 3 must be CRYPTO_BOX_PUBLICKEYBYTES bytes');
  163. }
  164. /** @var int $size */
  165. $size = filesize($inputFile);
  166. if (!is_int($size)) {
  167. throw new SodiumException('Could not obtain the file size');
  168. }
  169. /** @var resource $ifp */
  170. $ifp = fopen($inputFile, 'rb');
  171. if (!is_resource($ifp)) {
  172. throw new SodiumException('Could not open input file for reading');
  173. }
  174. /** @var resource $ofp */
  175. $ofp = fopen($outputFile, 'wb');
  176. if (!is_resource($ofp)) {
  177. fclose($ifp);
  178. throw new SodiumException('Could not open output file for writing');
  179. }
  180. /** @var string $ephKeypair */
  181. $ephKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair();
  182. /** @var string $msgKeypair */
  183. $msgKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair_from_secretkey_and_publickey(
  184. ParagonIE_Sodium_Compat::crypto_box_secretkey($ephKeypair),
  185. $publicKey
  186. );
  187. /** @var string $ephemeralPK */
  188. $ephemeralPK = ParagonIE_Sodium_Compat::crypto_box_publickey($ephKeypair);
  189. /** @var string $nonce */
  190. $nonce = ParagonIE_Sodium_Compat::crypto_generichash(
  191. $ephemeralPK . $publicKey,
  192. '',
  193. 24
  194. );
  195. /** @var int $firstWrite */
  196. $firstWrite = fwrite(
  197. $ofp,
  198. $ephemeralPK,
  199. ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES
  200. );
  201. if (!is_int($firstWrite)) {
  202. fclose($ifp);
  203. fclose($ofp);
  204. ParagonIE_Sodium_Compat::memzero($ephKeypair);
  205. throw new SodiumException('Could not write to output file');
  206. }
  207. if ($firstWrite !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) {
  208. ParagonIE_Sodium_Compat::memzero($ephKeypair);
  209. fclose($ifp);
  210. fclose($ofp);
  211. throw new SodiumException('Error writing public key to output file');
  212. }
  213. $res = self::box_encrypt($ifp, $ofp, $size, $nonce, $msgKeypair);
  214. fclose($ifp);
  215. fclose($ofp);
  216. try {
  217. ParagonIE_Sodium_Compat::memzero($nonce);
  218. ParagonIE_Sodium_Compat::memzero($ephKeypair);
  219. } catch (SodiumException $ex) {
  220. unset($ephKeypair);
  221. }
  222. return $res;
  223. }
  224. /**
  225. * Open a sealed file (rather than a string). Uses less memory than
  226. * ParagonIE_Sodium_Compat::crypto_box_seal_open(), but produces
  227. * the same result.
  228. *
  229. * Warning: Does not protect against TOCTOU attacks. You should
  230. * just load the file into memory and use crypto_box_seal_open() if
  231. * you are worried about those.
  232. *
  233. * @param string $inputFile
  234. * @param string $outputFile
  235. * @param string $ecdhKeypair
  236. * @return bool
  237. * @throws SodiumException
  238. * @throws TypeError
  239. */
  240. public static function box_seal_open($inputFile, $outputFile, $ecdhKeypair)
  241. {
  242. /* Type checks: */
  243. if (!is_string($inputFile)) {
  244. throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
  245. }
  246. if (!is_string($outputFile)) {
  247. throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
  248. }
  249. if (!is_string($ecdhKeypair)) {
  250. throw new TypeError('Argument 3 must be a string, ' . gettype($ecdhKeypair) . ' given.');
  251. }
  252. /* Input validation: */
  253. if (self::strlen($ecdhKeypair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) {
  254. throw new TypeError('Argument 3 must be CRYPTO_BOX_KEYPAIRBYTES bytes');
  255. }
  256. $publicKey = ParagonIE_Sodium_Compat::crypto_box_publickey($ecdhKeypair);
  257. /** @var int $size */
  258. $size = filesize($inputFile);
  259. if (!is_int($size)) {
  260. throw new SodiumException('Could not obtain the file size');
  261. }
  262. /** @var resource $ifp */
  263. $ifp = fopen($inputFile, 'rb');
  264. if (!is_resource($ifp)) {
  265. throw new SodiumException('Could not open input file for reading');
  266. }
  267. /** @var resource $ofp */
  268. $ofp = fopen($outputFile, 'wb');
  269. if (!is_resource($ofp)) {
  270. fclose($ifp);
  271. throw new SodiumException('Could not open output file for writing');
  272. }
  273. $ephemeralPK = fread($ifp, ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES);
  274. if (!is_string($ephemeralPK)) {
  275. throw new SodiumException('Could not read input file');
  276. }
  277. if (self::strlen($ephemeralPK) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) {
  278. fclose($ifp);
  279. fclose($ofp);
  280. throw new SodiumException('Could not read public key from sealed file');
  281. }
  282. $nonce = ParagonIE_Sodium_Compat::crypto_generichash(
  283. $ephemeralPK . $publicKey,
  284. '',
  285. 24
  286. );
  287. $msgKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair_from_secretkey_and_publickey(
  288. ParagonIE_Sodium_Compat::crypto_box_secretkey($ecdhKeypair),
  289. $ephemeralPK
  290. );
  291. $res = self::box_decrypt($ifp, $ofp, $size, $nonce, $msgKeypair);
  292. fclose($ifp);
  293. fclose($ofp);
  294. try {
  295. ParagonIE_Sodium_Compat::memzero($nonce);
  296. ParagonIE_Sodium_Compat::memzero($ephKeypair);
  297. } catch (SodiumException $ex) {
  298. unset($ephKeypair);
  299. }
  300. return $res;
  301. }
  302. /**
  303. * Calculate the BLAKE2b hash of a file.
  304. *
  305. * @param string $filePath Absolute path to a file on the filesystem
  306. * @param string|null $key BLAKE2b key
  307. * @param int $outputLength Length of hash output
  308. *
  309. * @return string BLAKE2b hash
  310. * @throws SodiumException
  311. * @throws TypeError
  312. * @psalm-suppress FailedTypeResolution
  313. */
  314. public static function generichash($filePath, $key = '', $outputLength = 32)
  315. {
  316. /* Type checks: */
  317. if (!is_string($filePath)) {
  318. throw new TypeError('Argument 1 must be a string, ' . gettype($filePath) . ' given.');
  319. }
  320. if (!is_string($key)) {
  321. if (is_null($key)) {
  322. $key = '';
  323. } else {
  324. throw new TypeError('Argument 2 must be a string, ' . gettype($key) . ' given.');
  325. }
  326. }
  327. if (!is_int($outputLength)) {
  328. if (!is_numeric($outputLength)) {
  329. throw new TypeError('Argument 3 must be an integer, ' . gettype($outputLength) . ' given.');
  330. }
  331. $outputLength = (int) $outputLength;
  332. }
  333. /* Input validation: */
  334. if (!empty($key)) {
  335. if (self::strlen($key) < ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_KEYBYTES_MIN) {
  336. throw new TypeError('Argument 2 must be at least CRYPTO_GENERICHASH_KEYBYTES_MIN bytes');
  337. }
  338. if (self::strlen($key) > ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_KEYBYTES_MAX) {
  339. throw new TypeError('Argument 2 must be at most CRYPTO_GENERICHASH_KEYBYTES_MAX bytes');
  340. }
  341. }
  342. if ($outputLength < ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_BYTES_MIN) {
  343. throw new SodiumException('Argument 3 must be at least CRYPTO_GENERICHASH_BYTES_MIN');
  344. }
  345. if ($outputLength > ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_BYTES_MAX) {
  346. throw new SodiumException('Argument 3 must be at least CRYPTO_GENERICHASH_BYTES_MAX');
  347. }
  348. /** @var int $size */
  349. $size = filesize($filePath);
  350. if (!is_int($size)) {
  351. throw new SodiumException('Could not obtain the file size');
  352. }
  353. /** @var resource $fp */
  354. $fp = fopen($filePath, 'rb');
  355. if (!is_resource($fp)) {
  356. throw new SodiumException('Could not open input file for reading');
  357. }
  358. $ctx = ParagonIE_Sodium_Compat::crypto_generichash_init($key, $outputLength);
  359. while ($size > 0) {
  360. $blockSize = $size > 64
  361. ? 64
  362. : $size;
  363. $read = fread($fp, $blockSize);
  364. if (!is_string($read)) {
  365. throw new SodiumException('Could not read input file');
  366. }
  367. ParagonIE_Sodium_Compat::crypto_generichash_update($ctx, $read);
  368. $size -= $blockSize;
  369. }
  370. fclose($fp);
  371. return ParagonIE_Sodium_Compat::crypto_generichash_final($ctx, $outputLength);
  372. }
  373. /**
  374. * Encrypt a file (rather than a string). Uses less memory than
  375. * ParagonIE_Sodium_Compat::crypto_secretbox(), but produces
  376. * the same result.
  377. *
  378. * @param string $inputFile Absolute path to a file on the filesystem
  379. * @param string $outputFile Absolute path to a file on the filesystem
  380. * @param string $nonce Number to be used only once
  381. * @param string $key Encryption key
  382. *
  383. * @return bool
  384. * @throws SodiumException
  385. * @throws TypeError
  386. */
  387. public static function secretbox($inputFile, $outputFile, $nonce, $key)
  388. {
  389. /* Type checks: */
  390. if (!is_string($inputFile)) {
  391. throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given..');
  392. }
  393. if (!is_string($outputFile)) {
  394. throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
  395. }
  396. if (!is_string($nonce)) {
  397. throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
  398. }
  399. /* Input validation: */
  400. if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_NONCEBYTES) {
  401. throw new TypeError('Argument 3 must be CRYPTO_SECRETBOX_NONCEBYTES bytes');
  402. }
  403. if (!is_string($key)) {
  404. throw new TypeError('Argument 4 must be a string, ' . gettype($key) . ' given.');
  405. }
  406. if (self::strlen($key) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_KEYBYTES) {
  407. throw new TypeError('Argument 4 must be CRYPTO_SECRETBOX_KEYBYTES bytes');
  408. }
  409. /** @var int $size */
  410. $size = filesize($inputFile);
  411. if (!is_int($size)) {
  412. throw new SodiumException('Could not obtain the file size');
  413. }
  414. /** @var resource $ifp */
  415. $ifp = fopen($inputFile, 'rb');
  416. if (!is_resource($ifp)) {
  417. throw new SodiumException('Could not open input file for reading');
  418. }
  419. /** @var resource $ofp */
  420. $ofp = fopen($outputFile, 'wb');
  421. if (!is_resource($ofp)) {
  422. fclose($ifp);
  423. throw new SodiumException('Could not open output file for writing');
  424. }
  425. $res = self::secretbox_encrypt($ifp, $ofp, $size, $nonce, $key);
  426. fclose($ifp);
  427. fclose($ofp);
  428. return $res;
  429. }
  430. /**
  431. * Seal a file (rather than a string). Uses less memory than
  432. * ParagonIE_Sodium_Compat::crypto_secretbox_open(), but produces
  433. * the same result.
  434. *
  435. * Warning: Does not protect against TOCTOU attacks. You should
  436. * just load the file into memory and use crypto_secretbox_open() if
  437. * you are worried about those.
  438. *
  439. * @param string $inputFile
  440. * @param string $outputFile
  441. * @param string $nonce
  442. * @param string $key
  443. * @return bool
  444. * @throws SodiumException
  445. * @throws TypeError
  446. */
  447. public static function secretbox_open($inputFile, $outputFile, $nonce, $key)
  448. {
  449. /* Type checks: */
  450. if (!is_string($inputFile)) {
  451. throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
  452. }
  453. if (!is_string($outputFile)) {
  454. throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
  455. }
  456. if (!is_string($nonce)) {
  457. throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
  458. }
  459. if (!is_string($key)) {
  460. throw new TypeError('Argument 4 must be a string, ' . gettype($key) . ' given.');
  461. }
  462. /* Input validation: */
  463. if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_NONCEBYTES) {
  464. throw new TypeError('Argument 4 must be CRYPTO_SECRETBOX_NONCEBYTES bytes');
  465. }
  466. if (self::strlen($key) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_KEYBYTES) {
  467. throw new TypeError('Argument 4 must be CRYPTO_SECRETBOXBOX_KEYBYTES bytes');
  468. }
  469. /** @var int $size */
  470. $size = filesize($inputFile);
  471. if (!is_int($size)) {
  472. throw new SodiumException('Could not obtain the file size');
  473. }
  474. /** @var resource $ifp */
  475. $ifp = fopen($inputFile, 'rb');
  476. if (!is_resource($ifp)) {
  477. throw new SodiumException('Could not open input file for reading');
  478. }
  479. /** @var resource $ofp */
  480. $ofp = fopen($outputFile, 'wb');
  481. if (!is_resource($ofp)) {
  482. fclose($ifp);
  483. throw new SodiumException('Could not open output file for writing');
  484. }
  485. $res = self::secretbox_decrypt($ifp, $ofp, $size, $nonce, $key);
  486. fclose($ifp);
  487. fclose($ofp);
  488. try {
  489. ParagonIE_Sodium_Compat::memzero($key);
  490. } catch (SodiumException $ex) {
  491. unset($key);
  492. }
  493. return $res;
  494. }
  495. /**
  496. * Sign a file (rather than a string). Uses less memory than
  497. * ParagonIE_Sodium_Compat::crypto_sign_detached(), but produces
  498. * the same result.
  499. *
  500. * @param string $filePath Absolute path to a file on the filesystem
  501. * @param string $secretKey Secret signing key
  502. *
  503. * @return string Ed25519 signature
  504. * @throws SodiumException
  505. * @throws TypeError
  506. */
  507. public static function sign($filePath, $secretKey)
  508. {
  509. /* Type checks: */
  510. if (!is_string($filePath)) {
  511. throw new TypeError('Argument 1 must be a string, ' . gettype($filePath) . ' given.');
  512. }
  513. if (!is_string($secretKey)) {
  514. throw new TypeError('Argument 2 must be a string, ' . gettype($secretKey) . ' given.');
  515. }
  516. /* Input validation: */
  517. if (self::strlen($secretKey) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_SECRETKEYBYTES) {
  518. throw new TypeError('Argument 2 must be CRYPTO_SIGN_SECRETKEYBYTES bytes');
  519. }
  520. if (PHP_INT_SIZE === 4) {
  521. return self::sign_core32($filePath, $secretKey);
  522. }
  523. /** @var int $size */
  524. $size = filesize($filePath);
  525. if (!is_int($size)) {
  526. throw new SodiumException('Could not obtain the file size');
  527. }
  528. /** @var resource $fp */
  529. $fp = fopen($filePath, 'rb');
  530. if (!is_resource($fp)) {
  531. throw new SodiumException('Could not open input file for reading');
  532. }
  533. /** @var string $az */
  534. $az = hash('sha512', self::substr($secretKey, 0, 32), true);
  535. $az[0] = self::intToChr(self::chrToInt($az[0]) & 248);
  536. $az[31] = self::intToChr((self::chrToInt($az[31]) & 63) | 64);
  537. $hs = hash_init('sha512');
  538. hash_update($hs, self::substr($az, 32, 32));
  539. /** @var resource $hs */
  540. $hs = self::updateHashWithFile($hs, $fp, $size);
  541. /** @var string $nonceHash */
  542. $nonceHash = hash_final($hs, true);
  543. /** @var string $pk */
  544. $pk = self::substr($secretKey, 32, 32);
  545. /** @var string $nonce */
  546. $nonce = ParagonIE_Sodium_Core_Ed25519::sc_reduce($nonceHash) . self::substr($nonceHash, 32);
  547. /** @var string $sig */
  548. $sig = ParagonIE_Sodium_Core_Ed25519::ge_p3_tobytes(
  549. ParagonIE_Sodium_Core_Ed25519::ge_scalarmult_base($nonce)
  550. );
  551. $hs = hash_init('sha512');
  552. hash_update($hs, self::substr($sig, 0, 32));
  553. hash_update($hs, self::substr($pk, 0, 32));
  554. /** @var resource $hs */
  555. $hs = self::updateHashWithFile($hs, $fp, $size);
  556. /** @var string $hramHash */
  557. $hramHash = hash_final($hs, true);
  558. /** @var string $hram */
  559. $hram = ParagonIE_Sodium_Core_Ed25519::sc_reduce($hramHash);
  560. /** @var string $sigAfter */
  561. $sigAfter = ParagonIE_Sodium_Core_Ed25519::sc_muladd($hram, $az, $nonce);
  562. /** @var string $sig */
  563. $sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32);
  564. try {
  565. ParagonIE_Sodium_Compat::memzero($az);
  566. } catch (SodiumException $ex) {
  567. $az = null;
  568. }
  569. fclose($fp);
  570. return $sig;
  571. }
  572. /**
  573. * Verify a file (rather than a string). Uses less memory than
  574. * ParagonIE_Sodium_Compat::crypto_sign_verify_detached(), but
  575. * produces the same result.
  576. *
  577. * @param string $sig Ed25519 signature
  578. * @param string $filePath Absolute path to a file on the filesystem
  579. * @param string $publicKey Signing public key
  580. *
  581. * @return bool
  582. * @throws SodiumException
  583. * @throws TypeError
  584. * @throws Exception
  585. */
  586. public static function verify($sig, $filePath, $publicKey)
  587. {
  588. /* Type checks: */
  589. if (!is_string($sig)) {
  590. throw new TypeError('Argument 1 must be a string, ' . gettype($sig) . ' given.');
  591. }
  592. if (!is_string($filePath)) {
  593. throw new TypeError('Argument 2 must be a string, ' . gettype($filePath) . ' given.');
  594. }
  595. if (!is_string($publicKey)) {
  596. throw new TypeError('Argument 3 must be a string, ' . gettype($publicKey) . ' given.');
  597. }
  598. /* Input validation: */
  599. if (self::strlen($sig) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_BYTES) {
  600. throw new TypeError('Argument 1 must be CRYPTO_SIGN_BYTES bytes');
  601. }
  602. if (self::strlen($publicKey) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_PUBLICKEYBYTES) {
  603. throw new TypeError('Argument 3 must be CRYPTO_SIGN_PUBLICKEYBYTES bytes');
  604. }
  605. if (self::strlen($sig) < 64) {
  606. throw new SodiumException('Signature is too short');
  607. }
  608. if (PHP_INT_SIZE === 4) {
  609. return self::verify_core32($sig, $filePath, $publicKey);
  610. }
  611. /* Security checks */
  612. if (ParagonIE_Sodium_Core_Ed25519::check_S_lt_L(self::substr($sig, 32, 32))) {
  613. throw new SodiumException('S < L - Invalid signature');
  614. }
  615. if (ParagonIE_Sodium_Core_Ed25519::small_order($sig)) {
  616. throw new SodiumException('Signature is on too small of an order');
  617. }
  618. if ((self::chrToInt($sig[63]) & 224) !== 0) {
  619. throw new SodiumException('Invalid signature');
  620. }
  621. $d = 0;
  622. for ($i = 0; $i < 32; ++$i) {
  623. $d |= self::chrToInt($publicKey[$i]);
  624. }
  625. if ($d === 0) {
  626. throw new SodiumException('All zero public key');
  627. }
  628. /** @var int $size */
  629. $size = filesize($filePath);
  630. if (!is_int($size)) {
  631. throw new SodiumException('Could not obtain the file size');
  632. }
  633. /** @var resource $fp */
  634. $fp = fopen($filePath, 'rb');
  635. if (!is_resource($fp)) {
  636. throw new SodiumException('Could not open input file for reading');
  637. }
  638. /** @var bool The original value of ParagonIE_Sodium_Compat::$fastMult */
  639. $orig = ParagonIE_Sodium_Compat::$fastMult;
  640. // Set ParagonIE_Sodium_Compat::$fastMult to true to speed up verification.
  641. ParagonIE_Sodium_Compat::$fastMult = true;
  642. /** @var ParagonIE_Sodium_Core_Curve25519_Ge_P3 $A */
  643. $A = ParagonIE_Sodium_Core_Ed25519::ge_frombytes_negate_vartime($publicKey);
  644. $hs = hash_init('sha512');
  645. hash_update($hs, self::substr($sig, 0, 32));
  646. hash_update($hs, self::substr($publicKey, 0, 32));
  647. /** @var resource $hs */
  648. $hs = self::updateHashWithFile($hs, $fp, $size);
  649. /** @var string $hDigest */
  650. $hDigest = hash_final($hs, true);
  651. /** @var string $h */
  652. $h = ParagonIE_Sodium_Core_Ed25519::sc_reduce($hDigest) . self::substr($hDigest, 32);
  653. /** @var ParagonIE_Sodium_Core_Curve25519_Ge_P2 $R */
  654. $R = ParagonIE_Sodium_Core_Ed25519::ge_double_scalarmult_vartime(
  655. $h,
  656. $A,
  657. self::substr($sig, 32)
  658. );
  659. /** @var string $rcheck */
  660. $rcheck = ParagonIE_Sodium_Core_Ed25519::ge_tobytes($R);
  661. // Close the file handle
  662. fclose($fp);
  663. // Reset ParagonIE_Sodium_Compat::$fastMult to what it was before.
  664. ParagonIE_Sodium_Compat::$fastMult = $orig;
  665. return self::verify_32($rcheck, self::substr($sig, 0, 32));
  666. }
  667. /**
  668. * @param resource $ifp
  669. * @param resource $ofp
  670. * @param int $mlen
  671. * @param string $nonce
  672. * @param string $boxKeypair
  673. * @return bool
  674. * @throws SodiumException
  675. * @throws TypeError
  676. */
  677. protected static function box_encrypt($ifp, $ofp, $mlen, $nonce, $boxKeypair)
  678. {
  679. if (PHP_INT_SIZE === 4) {
  680. return self::secretbox_encrypt(
  681. $ifp,
  682. $ofp,
  683. $mlen,
  684. $nonce,
  685. ParagonIE_Sodium_Crypto32::box_beforenm(
  686. ParagonIE_Sodium_Crypto32::box_secretkey($boxKeypair),
  687. ParagonIE_Sodium_Crypto32::box_publickey($boxKeypair)
  688. )
  689. );
  690. }
  691. return self::secretbox_encrypt(
  692. $ifp,
  693. $ofp,
  694. $mlen,
  695. $nonce,
  696. ParagonIE_Sodium_Crypto::box_beforenm(
  697. ParagonIE_Sodium_Crypto::box_secretkey($boxKeypair),
  698. ParagonIE_Sodium_Crypto::box_publickey($boxKeypair)
  699. )
  700. );
  701. }
  702. /**
  703. * @param resource $ifp
  704. * @param resource $ofp
  705. * @param int $mlen
  706. * @param string $nonce
  707. * @param string $boxKeypair
  708. * @return bool
  709. * @throws SodiumException
  710. * @throws TypeError
  711. */
  712. protected static function box_decrypt($ifp, $ofp, $mlen, $nonce, $boxKeypair)
  713. {
  714. if (PHP_INT_SIZE === 4) {
  715. return self::secretbox_decrypt(
  716. $ifp,
  717. $ofp,
  718. $mlen,
  719. $nonce,
  720. ParagonIE_Sodium_Crypto32::box_beforenm(
  721. ParagonIE_Sodium_Crypto32::box_secretkey($boxKeypair),
  722. ParagonIE_Sodium_Crypto32::box_publickey($boxKeypair)
  723. )
  724. );
  725. }
  726. return self::secretbox_decrypt(
  727. $ifp,
  728. $ofp,
  729. $mlen,
  730. $nonce,
  731. ParagonIE_Sodium_Crypto::box_beforenm(
  732. ParagonIE_Sodium_Crypto::box_secretkey($boxKeypair),
  733. ParagonIE_Sodium_Crypto::box_publickey($boxKeypair)
  734. )
  735. );
  736. }
  737. /**
  738. * Encrypt a file
  739. *
  740. * @param resource $ifp
  741. * @param resource $ofp
  742. * @param int $mlen
  743. * @param string $nonce
  744. * @param string $key
  745. * @return bool
  746. * @throws SodiumException
  747. * @throws TypeError
  748. */
  749. protected static function secretbox_encrypt($ifp, $ofp, $mlen, $nonce, $key)
  750. {
  751. if (PHP_INT_SIZE === 4) {
  752. return self::secretbox_encrypt_core32($ifp, $ofp, $mlen, $nonce, $key);
  753. }
  754. $plaintext = fread($ifp, 32);
  755. if (!is_string($plaintext)) {
  756. throw new SodiumException('Could not read input file');
  757. }
  758. $first32 = ftell($ifp);
  759. /** @var string $subkey */
  760. $subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key);
  761. /** @var string $realNonce */
  762. $realNonce = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8);
  763. /** @var string $block0 */
  764. $block0 = str_repeat("\x00", 32);
  765. /** @var int $mlen - Length of the plaintext message */
  766. $mlen0 = $mlen;
  767. if ($mlen0 > 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES) {
  768. $mlen0 = 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES;
  769. }
  770. $block0 .= ParagonIE_Sodium_Core_Util::substr($plaintext, 0, $mlen0);
  771. /** @var string $block0 */
  772. $block0 = ParagonIE_Sodium_Core_Salsa20::salsa20_xor(
  773. $block0,
  774. $realNonce,
  775. $subkey
  776. );
  777. $state = new ParagonIE_Sodium_Core_Poly1305_State(
  778. ParagonIE_Sodium_Core_Util::substr(
  779. $block0,
  780. 0,
  781. ParagonIE_Sodium_Crypto::onetimeauth_poly1305_KEYBYTES
  782. )
  783. );
  784. // Pre-write 16 blank bytes for the Poly1305 tag
  785. $start = ftell($ofp);
  786. fwrite($ofp, str_repeat("\x00", 16));
  787. /** @var string $c */
  788. $cBlock = ParagonIE_Sodium_Core_Util::substr(
  789. $block0,
  790. ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES
  791. );
  792. $state->update($cBlock);
  793. fwrite($ofp, $cBlock);
  794. $mlen -= 32;
  795. /** @var int $iter */
  796. $iter = 1;
  797. /** @var int $incr */
  798. $incr = self::BUFFER_SIZE >> 6;
  799. /*
  800. * Set the cursor to the end of the first half-block. All future bytes will
  801. * generated from salsa20_xor_ic, starting from 1 (second block).
  802. */
  803. fseek($ifp, $first32, SEEK_SET);
  804. while ($mlen > 0) {
  805. $blockSize = $mlen > self::BUFFER_SIZE
  806. ? self::BUFFER_SIZE
  807. : $mlen;
  808. $plaintext = fread($ifp, $blockSize);
  809. if (!is_string($plaintext)) {
  810. throw new SodiumException('Could not read input file');
  811. }
  812. $cBlock = ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic(
  813. $plaintext,
  814. $realNonce,
  815. $iter,
  816. $subkey
  817. );
  818. fwrite($ofp, $cBlock, $blockSize);
  819. $state->update($cBlock);
  820. $mlen -= $blockSize;
  821. $iter += $incr;
  822. }
  823. try {
  824. ParagonIE_Sodium_Compat::memzero($block0);
  825. ParagonIE_Sodium_Compat::memzero($subkey);
  826. } catch (SodiumException $ex) {
  827. $block0 = null;
  828. $subkey = null;
  829. }
  830. $end = ftell($ofp);
  831. /*
  832. * Write the Poly1305 authentication tag that provides integrity
  833. * over the ciphertext (encrypt-then-MAC)
  834. */
  835. fseek($ofp, $start, SEEK_SET);
  836. fwrite($ofp, $state->finish(), ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_MACBYTES);
  837. fseek($ofp, $end, SEEK_SET);
  838. unset($state);
  839. return true;
  840. }
  841. /**
  842. * Decrypt a file
  843. *
  844. * @param resource $ifp
  845. * @param resource $ofp
  846. * @param int $mlen
  847. * @param string $nonce
  848. * @param string $key
  849. * @return bool
  850. * @throws SodiumException
  851. * @throws TypeError
  852. */
  853. protected static function secretbox_decrypt($ifp, $ofp, $mlen, $nonce, $key)
  854. {
  855. if (PHP_INT_SIZE === 4) {
  856. return self::secretbox_decrypt_core32($ifp, $ofp, $mlen, $nonce, $key);
  857. }
  858. $tag = fread($ifp, 16);
  859. if (!is_string($tag)) {
  860. throw new SodiumException('Could not read input file');
  861. }
  862. /** @var string $subkey */
  863. $subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key);
  864. /** @var string $realNonce */
  865. $realNonce = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8);
  866. /** @var string $block0 */
  867. $block0 = ParagonIE_Sodium_Core_Salsa20::salsa20(
  868. 64,
  869. ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8),
  870. $subkey
  871. );
  872. /* Verify the Poly1305 MAC -before- attempting to decrypt! */
  873. $state = new ParagonIE_Sodium_Core_Poly1305_State(self::substr($block0, 0, 32));
  874. if (!self::onetimeauth_verify($state, $ifp, $tag, $mlen)) {
  875. throw new SodiumException('Invalid MAC');
  876. }
  877. /*
  878. * Set the cursor to the end of the first half-block. All future bytes will
  879. * generated from salsa20_xor_ic, starting from 1 (second block).
  880. */
  881. $first32 = fread($ifp, 32);
  882. if (!is_string($first32)) {
  883. throw new SodiumException('Could not read input file');
  884. }
  885. $first32len = self::strlen($first32);
  886. fwrite(
  887. $ofp,
  888. self::xorStrings(
  889. self::substr($block0, 32, $first32len),
  890. self::substr($first32, 0, $first32len)
  891. )
  892. );
  893. $mlen -= 32;
  894. /** @var int $iter */
  895. $iter = 1;
  896. /** @var int $incr */
  897. $incr = self::BUFFER_SIZE >> 6;
  898. /* Decrypts ciphertext, writes to output file. */
  899. while ($mlen > 0) {
  900. $blockSize = $mlen > self::BUFFER_SIZE
  901. ? self::BUFFER_SIZE
  902. : $mlen;
  903. $ciphertext = fread($ifp, $blockSize);
  904. if (!is_string($ciphertext)) {
  905. throw new SodiumException('Could not read input file');
  906. }
  907. $pBlock = ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic(
  908. $ciphertext,
  909. $realNonce,
  910. $iter,
  911. $subkey
  912. );
  913. fwrite($ofp, $pBlock, $blockSize);
  914. $mlen -= $blockSize;
  915. $iter += $incr;
  916. }
  917. return true;
  918. }
  919. /**
  920. * @param ParagonIE_Sodium_Core_Poly1305_State $state
  921. * @param resource $ifp
  922. * @param string $tag
  923. * @param int $mlen
  924. * @return bool
  925. * @throws SodiumException
  926. * @throws TypeError
  927. */
  928. protected static function onetimeauth_verify(
  929. ParagonIE_Sodium_Core_Poly1305_State $state,
  930. $ifp,
  931. $tag = '',
  932. $mlen = 0
  933. ) {
  934. /** @var int $pos */
  935. $pos = ftell($ifp);
  936. /** @var int $iter */
  937. $iter = 1;
  938. /** @var int $incr */
  939. $incr = self::BUFFER_SIZE >> 6;
  940. while ($mlen > 0) {
  941. $blockSize = $mlen > self::BUFFER_SIZE
  942. ? self::BUFFER_SIZE
  943. : $mlen;
  944. $ciphertext = fread($ifp, $blockSize);
  945. if (!is_string($ciphertext)) {
  946. throw new SodiumException('Could not read input file');
  947. }
  948. $state->update($ciphertext);
  949. $mlen -= $blockSize;
  950. $iter += $incr;
  951. }
  952. $res = ParagonIE_Sodium_Core_Util::verify_16($tag, $state->finish());
  953. fseek($ifp, $pos, SEEK_SET);
  954. return $res;
  955. }
  956. /**
  957. * Update a hash context with the contents of a file, without
  958. * loading the entire file into memory.
  959. *
  960. * @param resource|object $hash
  961. * @param resource $fp
  962. * @param int $size
  963. * @return resource|object Resource on PHP < 7.2, HashContext object on PHP >= 7.2
  964. * @throws SodiumException
  965. * @throws TypeError
  966. * @psalm-suppress PossiblyInvalidArgument
  967. * PHP 7.2 changes from a resource to an object,
  968. * which causes Psalm to complain about an error.
  969. * @psalm-suppress TypeCoercion
  970. * Ditto.
  971. */
  972. public static function updateHashWithFile($hash, $fp, $size = 0)
  973. {
  974. /* Type checks: */
  975. if (PHP_VERSION_ID < 70200) {
  976. if (!is_resource($hash)) {
  977. throw new TypeError('Argument 1 must be a resource, ' . gettype($hash) . ' given.');
  978. }
  979. } else {
  980. if (!is_object($hash)) {
  981. throw new TypeError('Argument 1 must be an object (PHP 7.2+), ' . gettype($hash) . ' given.');
  982. }
  983. }
  984. if (!is_resource($fp)) {
  985. throw new TypeError('Argument 2 must be a resource, ' . gettype($fp) . ' given.');
  986. }
  987. if (!is_int($size)) {
  988. throw new TypeError('Argument 3 must be an integer, ' . gettype($size) . ' given.');
  989. }
  990. /** @var int $originalPosition */
  991. $originalPosition = ftell($fp);
  992. // Move file pointer to beginning of file
  993. fseek($fp, 0, SEEK_SET);
  994. for ($i = 0; $i < $size; $i += self::BUFFER_SIZE) {
  995. /** @var string|bool $message */
  996. $message = fread(
  997. $fp,
  998. ($size - $i) > self::BUFFER_SIZE
  999. ? $size - $i
  1000. : self::BUFFER_SIZE
  1001. );
  1002. if (!is_string($message)) {
  1003. throw new SodiumException('Unexpected error reading from file.');
  1004. }
  1005. /** @var string $message */
  1006. /** @psalm-suppress InvalidArgument */
  1007. hash_update($hash, $message);
  1008. }
  1009. // Reset file pointer's position
  1010. fseek($fp, $originalPosition, SEEK_SET);
  1011. return $hash;
  1012. }
  1013. /**
  1014. * Sign a file (rather than a string). Uses less memory than
  1015. * ParagonIE_Sodium_Compat::crypto_sign_detached(), but produces
  1016. * the same result. (32-bit)
  1017. *
  1018. * @param string $filePath Absolute path to a file on the filesystem
  1019. * @param string $secretKey Secret signing key
  1020. *
  1021. * @return string Ed25519 signature
  1022. * @throws SodiumException
  1023. * @throws TypeError
  1024. */
  1025. private static function sign_core32($filePath, $secretKey)
  1026. {
  1027. /** @var int|bool $size */
  1028. $size = filesize($filePath);
  1029. if (!is_int($size)) {
  1030. throw new SodiumException('Could not obtain the file size');
  1031. }
  1032. /** @var int $size */
  1033. /** @var resource|bool $fp */
  1034. $fp = fopen($filePath, 'rb');
  1035. if (!is_resource($fp)) {
  1036. throw new SodiumException('Could not open input file for reading');
  1037. }
  1038. /** @var resource $fp */
  1039. /** @var string $az */
  1040. $az = hash('sha512', self::substr($secretKey, 0, 32), true);
  1041. $az[0] = self::intToChr(self::chrToInt($az[0]) & 248);
  1042. $az[31] = self::intToChr((self::chrToInt($az[31]) & 63) | 64);
  1043. $hs = hash_init('sha512');
  1044. hash_update($hs, self::substr($az, 32, 32));
  1045. /** @var resource $hs */
  1046. $hs = self::updateHashWithFile($hs, $fp, $size);
  1047. /** @var string $nonceHash */
  1048. $nonceHash = hash_final($hs, true);
  1049. /** @var string $pk */
  1050. $pk = self::substr($secretKey, 32, 32);
  1051. /** @var string $nonce */
  1052. $nonce = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($nonceHash) . self::substr($nonceHash, 32);
  1053. /** @var string $sig */
  1054. $sig = ParagonIE_Sodium_Core32_Ed25519::ge_p3_tobytes(
  1055. ParagonIE_Sodium_Core32_Ed25519::ge_scalarmult_base($nonce)
  1056. );
  1057. $hs = hash_init('sha512');
  1058. hash_update($hs, self::substr($sig, 0, 32));
  1059. hash_update($hs, self::substr($pk, 0, 32));
  1060. /** @var resource $hs */
  1061. $hs = self::updateHashWithFile($hs, $fp, $size);
  1062. /** @var string $hramHash */
  1063. $hramHash = hash_final($hs, true);
  1064. /** @var string $hram */
  1065. $hram = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($hramHash);
  1066. /** @var string $sigAfter */
  1067. $sigAfter = ParagonIE_Sodium_Core32_Ed25519::sc_muladd($hram, $az, $nonce);
  1068. /** @var string $sig */
  1069. $sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32);
  1070. try {
  1071. ParagonIE_Sodium_Compat::memzero($az);
  1072. } catch (SodiumException $ex) {
  1073. $az = null;
  1074. }
  1075. fclose($fp);
  1076. return $sig;
  1077. }
  1078. /**
  1079. *
  1080. * Verify a file (rather than a string). Uses less memory than
  1081. * ParagonIE_Sodium_Compat::crypto_sign_verify_detached(), but
  1082. * produces the same result. (32-bit)
  1083. *
  1084. * @param string $sig Ed25519 signature
  1085. * @param string $filePath Absolute path to a file on the filesystem
  1086. * @param string $publicKey Signing public key
  1087. *
  1088. * @return bool
  1089. * @throws SodiumException
  1090. * @throws Exception
  1091. */
  1092. public static function verify_core32($sig, $filePath, $publicKey)
  1093. {
  1094. /* Security checks */
  1095. if (ParagonIE_Sodium_Core32_Ed25519::check_S_lt_L(self::substr($sig, 32, 32))) {
  1096. throw new SodiumException('S < L - Invalid signature');
  1097. }
  1098. if (ParagonIE_Sodium_Core32_Ed25519::small_order($sig)) {
  1099. throw new SodiumException('Signature is on too small of an order');
  1100. }
  1101. if ((self::chrToInt($sig[63]) & 224) !== 0) {
  1102. throw new SodiumException('Invalid signature');
  1103. }
  1104. $d = 0;
  1105. for ($i = 0; $i < 32; ++$i) {
  1106. $d |= self::chrToInt($publicKey[$i]);
  1107. }
  1108. if ($d === 0) {
  1109. throw new SodiumException('All zero public key');
  1110. }
  1111. /** @var int|bool $size */
  1112. $size = filesize($filePath);
  1113. if (!is_int($size)) {
  1114. throw new SodiumException('Could not obtain the file size');
  1115. }
  1116. /** @var int $size */
  1117. /** @var resource|bool $fp */
  1118. $fp = fopen($filePath, 'rb');
  1119. if (!is_resource($fp)) {
  1120. throw new SodiumException('Could not open input file for reading');
  1121. }
  1122. /** @var resource $fp */
  1123. /** @var bool The original value of ParagonIE_Sodium_Compat::$fastMult */
  1124. $orig = ParagonIE_Sodium_Compat::$fastMult;
  1125. // Set ParagonIE_Sodium_Compat::$fastMult to true to speed up verification.
  1126. ParagonIE_Sodium_Compat::$fastMult = true;
  1127. /** @var ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $A */
  1128. $A = ParagonIE_Sodium_Core32_Ed25519::ge_frombytes_negate_vartime($publicKey);
  1129. $hs = hash_init('sha512');
  1130. hash_update($hs, self::substr($sig, 0, 32));
  1131. hash_update($hs, self::substr($publicKey, 0, 32));
  1132. /** @var resource $hs */
  1133. $hs = self::updateHashWithFile($hs, $fp, $size);
  1134. /** @var string $hDigest */
  1135. $hDigest = hash_final($hs, true);
  1136. /** @var string $h */
  1137. $h = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($hDigest) . self::substr($hDigest, 32);
  1138. /** @var ParagonIE_Sodium_Core32_Curve25519_Ge_P2 $R */
  1139. $R = ParagonIE_Sodium_Core32_Ed25519::ge_double_scalarmult_vartime(
  1140. $h,
  1141. $A,
  1142. self::substr($sig, 32)
  1143. );
  1144. /** @var string $rcheck */
  1145. $rcheck = ParagonIE_Sodium_Core32_Ed25519::ge_tobytes($R);
  1146. // Close the file handle
  1147. fclose($fp);
  1148. // Reset ParagonIE_Sodium_Compat::$fastMult to what it was before.
  1149. ParagonIE_Sodium_Compat::$fastMult = $orig;
  1150. return self::verify_32($rcheck, self::substr($sig, 0, 32));
  1151. }
  1152. /**
  1153. * Encrypt a file (32-bit)
  1154. *
  1155. * @param resource $ifp
  1156. * @param resource $ofp
  1157. * @param int $mlen
  1158. * @param string $nonce
  1159. * @param string $key
  1160. * @return bool
  1161. * @throws SodiumException
  1162. * @throws TypeError
  1163. */
  1164. protected static function secretbox_encrypt_core32($ifp, $ofp, $mlen, $nonce, $key)
  1165. {
  1166. $plaintext = fread($ifp, 32);
  1167. if (!is_string($plaintext)) {
  1168. throw new SodiumException('Could not read input file');
  1169. }
  1170. $first32 = ftell($ifp);
  1171. /** @var string $subkey */
  1172. $subkey = ParagonIE_Sodium_Core32_HSalsa20::hsalsa20($nonce, $key);
  1173. /** @var string $realNonce */
  1174. $realNonce = ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8);
  1175. /** @var string $block0 */
  1176. $block0 = str_repeat("\x00", 32);
  1177. /** @var int $mlen - Length of the plaintext message */
  1178. $mlen0 = $mlen;
  1179. if ($mlen0 > 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES) {
  1180. $mlen0 = 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES;
  1181. }
  1182. $block0 .= ParagonIE_Sodium_Core32_Util::substr($plaintext, 0, $mlen0);
  1183. /** @var string $block0 */
  1184. $block0 = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor(
  1185. $block0,
  1186. $realNonce,
  1187. $subkey
  1188. );
  1189. $state = new ParagonIE_Sodium_Core32_Poly1305_State(
  1190. ParagonIE_Sodium_Core32_Util::substr(
  1191. $block0,
  1192. 0,
  1193. ParagonIE_Sodium_Crypto::onetimeauth_poly1305_KEYBYTES
  1194. )
  1195. );
  1196. // Pre-write 16 blank bytes for the Poly1305 tag
  1197. $start = ftell($ofp);
  1198. fwrite($ofp, str_repeat("\x00", 16));
  1199. /** @var string $c */
  1200. $cBlock = ParagonIE_Sodium_Core32_Util::substr(
  1201. $block0,
  1202. ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES
  1203. );
  1204. $state->update($cBlock);
  1205. fwrite($ofp, $cBlock);
  1206. $mlen -= 32;
  1207. /** @var int $iter */
  1208. $iter = 1;
  1209. /** @var int $incr */
  1210. $incr = self::BUFFER_SIZE >> 6;
  1211. /*
  1212. * Set the cursor to the end of the first half-block. All future bytes will
  1213. * generated from salsa20_xor_ic, starting from 1 (second block).
  1214. */
  1215. fseek($ifp, $first32, SEEK_SET);
  1216. while ($mlen > 0) {
  1217. $blockSize = $mlen > self::BUFFER_SIZE
  1218. ? self::BUFFER_SIZE
  1219. : $mlen;
  1220. $plaintext = fread($ifp, $blockSize);
  1221. if (!is_string($plaintext)) {
  1222. throw new SodiumException('Could not read input file');
  1223. }
  1224. $cBlock = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor_ic(
  1225. $plaintext,
  1226. $realNonce,
  1227. $iter,
  1228. $subkey
  1229. );
  1230. fwrite($ofp, $cBlock, $blockSize);
  1231. $state->update($cBlock);
  1232. $mlen -= $blockSize;
  1233. $iter += $incr;
  1234. }
  1235. try {
  1236. ParagonIE_Sodium_Compat::memzero($block0);
  1237. ParagonIE_Sodium_Compat::memzero($subkey);
  1238. } catch (SodiumException $ex) {
  1239. $block0 = null;
  1240. $subkey = null;
  1241. }
  1242. $end = ftell($ofp);
  1243. /*
  1244. * Write the Poly1305 authentication tag that provides integrity
  1245. * over the ciphertext (encrypt-then-MAC)
  1246. */
  1247. fseek($ofp, $start, SEEK_SET);
  1248. fwrite($ofp, $state->finish(), ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_MACBYTES);
  1249. fseek($ofp, $end, SEEK_SET);
  1250. unset($state);
  1251. return true;
  1252. }
  1253. /**
  1254. * Decrypt a file (32-bit)
  1255. *
  1256. * @param resource $ifp
  1257. * @param resource $ofp
  1258. * @param int $mlen
  1259. * @param string $nonce
  1260. * @param string $key
  1261. * @return bool
  1262. * @throws SodiumException
  1263. * @throws TypeError
  1264. */
  1265. protected static function secretbox_decrypt_core32($ifp, $ofp, $mlen, $nonce, $key)
  1266. {
  1267. $tag = fread($ifp, 16);
  1268. if (!is_string($tag)) {
  1269. throw new SodiumException('Could not read input file');
  1270. }
  1271. /** @var string $subkey */
  1272. $subkey = ParagonIE_Sodium_Core32_HSalsa20::hsalsa20($nonce, $key);
  1273. /** @var string $realNonce */
  1274. $realNonce = ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8);
  1275. /** @var string $block0 */
  1276. $block0 = ParagonIE_Sodium_Core32_Salsa20::salsa20(
  1277. 64,
  1278. ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8),
  1279. $subkey
  1280. );
  1281. /* Verify the Poly1305 MAC -before- attempting to decrypt! */
  1282. $state = new ParagonIE_Sodium_Core32_Poly1305_State(self::substr($block0, 0, 32));
  1283. if (!self::onetimeauth_verify_core32($state, $ifp, $tag, $mlen)) {
  1284. throw new SodiumException('Invalid MAC');
  1285. }
  1286. /*
  1287. * Set the cursor to the end of the first half-block. All future bytes will
  1288. * generated from salsa20_xor_ic, starting from 1 (second block).
  1289. */
  1290. $first32 = fread($ifp, 32);
  1291. if (!is_string($first32)) {
  1292. throw new SodiumException('Could not read input file');
  1293. }
  1294. $first32len = self::strlen($first32);
  1295. fwrite(
  1296. $ofp,
  1297. self::xorStrings(
  1298. self::substr($block0, 32, $first32len),
  1299. self::substr($first32, 0, $first32len)
  1300. )
  1301. );
  1302. $mlen -= 32;
  1303. /** @var int $iter */
  1304. $iter = 1;
  1305. /** @var int $incr */
  1306. $incr = self::BUFFER_SIZE >> 6;
  1307. /* Decrypts ciphertext, writes to output file. */
  1308. while ($mlen > 0) {
  1309. $blockSize = $mlen > self::BUFFER_SIZE
  1310. ? self::BUFFER_SIZE
  1311. : $mlen;
  1312. $ciphertext = fread($ifp, $blockSize);
  1313. if (!is_string($ciphertext)) {
  1314. throw new SodiumException('Could not read input file');
  1315. }
  1316. $pBlock = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor_ic(
  1317. $ciphertext,
  1318. $realNonce,
  1319. $iter,
  1320. $subkey
  1321. );
  1322. fwrite($ofp, $pBlock, $blockSize);
  1323. $mlen -= $blockSize;
  1324. $iter += $incr;
  1325. }
  1326. return true;
  1327. }
  1328. /**
  1329. * One-time message authentication for 32-bit systems
  1330. *
  1331. * @param ParagonIE_Sodium_Core32_Poly1305_State $state
  1332. * @param resource $ifp
  1333. * @param string $tag
  1334. * @param int $mlen
  1335. * @return bool
  1336. * @throws SodiumException
  1337. * @throws TypeError
  1338. */
  1339. protected static function onetimeauth_verify_core32(
  1340. ParagonIE_Sodium_Core32_Poly1305_State $state,
  1341. $ifp,
  1342. $tag = '',
  1343. $mlen = 0
  1344. ) {
  1345. /** @var int $pos */
  1346. $pos = ftell($ifp);
  1347. /** @var int $iter */
  1348. $iter = 1;
  1349. /** @var int $incr */
  1350. $incr = self::BUFFER_SIZE >> 6;
  1351. while ($mlen > 0) {
  1352. $blockSize = $mlen > self::BUFFER_SIZE
  1353. ? self::BUFFER_SIZE
  1354. : $mlen;
  1355. $ciphertext = fread($ifp, $blockSize);
  1356. if (!is_string($ciphertext)) {
  1357. throw new SodiumException('Could not read input file');
  1358. }
  1359. $state->update($ciphertext);
  1360. $mlen -= $blockSize;
  1361. $iter += $incr;
  1362. }
  1363. $res = ParagonIE_Sodium_Core32_Util::verify_16($tag, $state->finish());
  1364. fseek($ifp, $pos, SEEK_SET);
  1365. return $res;
  1366. }
  1367. }