Crypt.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Bjoern Schiessle <bjoern@schiessle.org>
  6. * @author Björn Schießle <bjoern@schiessle.org>
  7. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  8. * @author Clark Tomlinson <fallen013@gmail.com>
  9. * @author Côme Chilliet <come.chilliet@nextcloud.com>
  10. * @author Joas Schilling <coding@schilljs.com>
  11. * @author Kevin Niehage <kevin@niehage.name>
  12. * @author Lukas Reschke <lukas@statuscode.ch>
  13. * @author Morris Jobke <hey@morrisjobke.de>
  14. * @author Roeland Jago Douma <roeland@famdouma.nl>
  15. * @author Stefan Weiberg <sweiberg@suse.com>
  16. * @author Thomas Müller <thomas.mueller@tmit.eu>
  17. *
  18. * @license AGPL-3.0
  19. *
  20. * This code is free software: you can redistribute it and/or modify
  21. * it under the terms of the GNU Affero General Public License, version 3,
  22. * as published by the Free Software Foundation.
  23. *
  24. * This program is distributed in the hope that it will be useful,
  25. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  26. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  27. * GNU Affero General Public License for more details.
  28. *
  29. * You should have received a copy of the GNU Affero General Public License, version 3,
  30. * along with this program. If not, see <http://www.gnu.org/licenses/>
  31. *
  32. */
  33. namespace OCA\Encryption\Crypto;
  34. use OC\Encryption\Exceptions\DecryptionFailedException;
  35. use OC\Encryption\Exceptions\EncryptionFailedException;
  36. use OC\ServerNotAvailableException;
  37. use OCA\Encryption\Exceptions\MultiKeyDecryptException;
  38. use OCA\Encryption\Exceptions\MultiKeyEncryptException;
  39. use OCP\Encryption\Exceptions\GenericEncryptionException;
  40. use OCP\IConfig;
  41. use OCP\IL10N;
  42. use OCP\ILogger;
  43. use OCP\IUserSession;
  44. use phpseclib\Crypt\RC4;
  45. /**
  46. * Class Crypt provides the encryption implementation of the default Nextcloud
  47. * encryption module. As default AES-256-CTR is used, it does however offer support
  48. * for the following modes:
  49. *
  50. * - AES-256-CTR
  51. * - AES-128-CTR
  52. * - AES-256-CFB
  53. * - AES-128-CFB
  54. *
  55. * For integrity protection Encrypt-Then-MAC using HMAC-SHA256 is used.
  56. *
  57. * @package OCA\Encryption\Crypto
  58. */
  59. class Crypt {
  60. public const SUPPORTED_CIPHERS_AND_KEY_SIZE = [
  61. 'AES-256-CTR' => 32,
  62. 'AES-128-CTR' => 16,
  63. 'AES-256-CFB' => 32,
  64. 'AES-128-CFB' => 16,
  65. ];
  66. // one out of SUPPORTED_CIPHERS_AND_KEY_SIZE
  67. public const DEFAULT_CIPHER = 'AES-256-CTR';
  68. // default cipher from old Nextcloud versions
  69. public const LEGACY_CIPHER = 'AES-128-CFB';
  70. public const SUPPORTED_KEY_FORMATS = ['hash2', 'hash', 'password'];
  71. // one out of SUPPORTED_KEY_FORMATS
  72. public const DEFAULT_KEY_FORMAT = 'hash2';
  73. // default key format, old Nextcloud version encrypted the private key directly
  74. // with the user password
  75. public const LEGACY_KEY_FORMAT = 'password';
  76. public const HEADER_START = 'HBEGIN';
  77. public const HEADER_END = 'HEND';
  78. // default encoding format, old Nextcloud versions used base64
  79. public const BINARY_ENCODING_FORMAT = 'binary';
  80. /** @var ILogger */
  81. private $logger;
  82. /** @var string */
  83. private $user;
  84. /** @var IConfig */
  85. private $config;
  86. /** @var IL10N */
  87. private $l;
  88. /** @var string|null */
  89. private $currentCipher;
  90. /** @var bool */
  91. private $supportLegacy;
  92. /**
  93. * Use the legacy base64 encoding instead of the more space-efficient binary encoding.
  94. */
  95. private bool $useLegacyBase64Encoding;
  96. /**
  97. * @param ILogger $logger
  98. * @param IUserSession $userSession
  99. * @param IConfig $config
  100. * @param IL10N $l
  101. */
  102. public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
  103. $this->logger = $logger;
  104. $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
  105. $this->config = $config;
  106. $this->l = $l;
  107. $this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false);
  108. $this->useLegacyBase64Encoding = $this->config->getSystemValueBool('encryption.use_legacy_base64_encoding', false);
  109. }
  110. /**
  111. * create new private/public key-pair for user
  112. *
  113. * @return array|bool
  114. */
  115. public function createKeyPair() {
  116. $log = $this->logger;
  117. $res = $this->getOpenSSLPKey();
  118. if (!$res) {
  119. $log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
  120. ['app' => 'encryption']);
  121. if (openssl_error_string()) {
  122. $log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
  123. ['app' => 'encryption']);
  124. }
  125. } elseif (openssl_pkey_export($res,
  126. $privateKey,
  127. null,
  128. $this->getOpenSSLConfig())) {
  129. $keyDetails = openssl_pkey_get_details($res);
  130. $publicKey = $keyDetails['key'];
  131. return [
  132. 'publicKey' => $publicKey,
  133. 'privateKey' => $privateKey
  134. ];
  135. }
  136. $log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
  137. ['app' => 'encryption']);
  138. if (openssl_error_string()) {
  139. $log->error('Encryption Library:' . openssl_error_string(),
  140. ['app' => 'encryption']);
  141. }
  142. return false;
  143. }
  144. /**
  145. * Generates a new private key
  146. *
  147. * @return \OpenSSLAsymmetricKey|false
  148. */
  149. public function getOpenSSLPKey() {
  150. $config = $this->getOpenSSLConfig();
  151. return openssl_pkey_new($config);
  152. }
  153. /**
  154. * get openSSL Config
  155. *
  156. * @return array
  157. */
  158. private function getOpenSSLConfig() {
  159. $config = ['private_key_bits' => 4096];
  160. $config = array_merge(
  161. $config,
  162. $this->config->getSystemValue('openssl', [])
  163. );
  164. return $config;
  165. }
  166. /**
  167. * @param string $plainContent
  168. * @param string $passPhrase
  169. * @param int $version
  170. * @param int $position
  171. * @return false|string
  172. * @throws EncryptionFailedException
  173. */
  174. public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
  175. if (!$plainContent) {
  176. $this->logger->error('Encryption Library, symmetrical encryption failed no content given',
  177. ['app' => 'encryption']);
  178. return false;
  179. }
  180. $iv = $this->generateIv();
  181. $encryptedContent = $this->encrypt($plainContent,
  182. $iv,
  183. $passPhrase,
  184. $this->getCipher());
  185. // Create a signature based on the key as well as the current version
  186. $sig = $this->createSignature($encryptedContent, $passPhrase.'_'.$version.'_'.$position);
  187. // combine content to encrypt the IV identifier and actual IV
  188. $catFile = $this->concatIV($encryptedContent, $iv);
  189. $catFile = $this->concatSig($catFile, $sig);
  190. return $this->addPadding($catFile);
  191. }
  192. /**
  193. * generate header for encrypted file
  194. *
  195. * @param string $keyFormat see SUPPORTED_KEY_FORMATS
  196. * @return string
  197. * @throws \InvalidArgumentException
  198. */
  199. public function generateHeader($keyFormat = self::DEFAULT_KEY_FORMAT) {
  200. if (in_array($keyFormat, self::SUPPORTED_KEY_FORMATS, true) === false) {
  201. throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
  202. }
  203. $header = self::HEADER_START
  204. . ':cipher:' . $this->getCipher()
  205. . ':keyFormat:' . $keyFormat;
  206. if ($this->useLegacyBase64Encoding !== true) {
  207. $header .= ':encoding:' . self::BINARY_ENCODING_FORMAT;
  208. }
  209. $header .= ':' . self::HEADER_END;
  210. return $header;
  211. }
  212. /**
  213. * @param string $plainContent
  214. * @param string $iv
  215. * @param string $passPhrase
  216. * @param string $cipher
  217. * @return string
  218. * @throws EncryptionFailedException
  219. */
  220. private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
  221. $options = $this->useLegacyBase64Encoding ? 0 : OPENSSL_RAW_DATA;
  222. $encryptedContent = openssl_encrypt($plainContent,
  223. $cipher,
  224. $passPhrase,
  225. $options,
  226. $iv);
  227. if (!$encryptedContent) {
  228. $error = 'Encryption (symmetric) of content failed';
  229. $this->logger->error($error . openssl_error_string(),
  230. ['app' => 'encryption']);
  231. throw new EncryptionFailedException($error);
  232. }
  233. return $encryptedContent;
  234. }
  235. /**
  236. * return cipher either from config.php or the default cipher defined in
  237. * this class
  238. *
  239. * @return string
  240. */
  241. private function getCachedCipher() {
  242. if (isset($this->currentCipher)) {
  243. return $this->currentCipher;
  244. }
  245. // Get cipher either from config.php or the default cipher defined in this class
  246. $cipher = $this->config->getSystemValueString('cipher', self::DEFAULT_CIPHER);
  247. if (!isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
  248. $this->logger->warning(
  249. sprintf(
  250. 'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
  251. $cipher,
  252. self::DEFAULT_CIPHER
  253. ),
  254. ['app' => 'encryption']
  255. );
  256. $cipher = self::DEFAULT_CIPHER;
  257. }
  258. // Remember current cipher to avoid frequent lookups
  259. $this->currentCipher = $cipher;
  260. return $this->currentCipher;
  261. }
  262. /**
  263. * return current encryption cipher
  264. *
  265. * @return string
  266. */
  267. public function getCipher() {
  268. return $this->getCachedCipher();
  269. }
  270. /**
  271. * get key size depending on the cipher
  272. *
  273. * @param string $cipher
  274. * @return int
  275. * @throws \InvalidArgumentException
  276. */
  277. protected function getKeySize($cipher) {
  278. if (isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
  279. return self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher];
  280. }
  281. throw new \InvalidArgumentException(
  282. sprintf(
  283. 'Unsupported cipher (%s) defined.',
  284. $cipher
  285. )
  286. );
  287. }
  288. /**
  289. * get legacy cipher
  290. *
  291. * @return string
  292. */
  293. public function getLegacyCipher() {
  294. if (!$this->supportLegacy) {
  295. throw new ServerNotAvailableException('Legacy cipher is no longer supported!');
  296. }
  297. return self::LEGACY_CIPHER;
  298. }
  299. /**
  300. * @param string $encryptedContent
  301. * @param string $iv
  302. * @return string
  303. */
  304. private function concatIV($encryptedContent, $iv) {
  305. return $encryptedContent . '00iv00' . $iv;
  306. }
  307. /**
  308. * @param string $encryptedContent
  309. * @param string $signature
  310. * @return string
  311. */
  312. private function concatSig($encryptedContent, $signature) {
  313. return $encryptedContent . '00sig00' . $signature;
  314. }
  315. /**
  316. * Note: This is _NOT_ a padding used for encryption purposes. It is solely
  317. * used to achieve the PHP stream size. It has _NOTHING_ to do with the
  318. * encrypted content and is not used in any crypto primitive.
  319. *
  320. * @param string $data
  321. * @return string
  322. */
  323. private function addPadding($data) {
  324. return $data . 'xxx';
  325. }
  326. /**
  327. * generate password hash used to encrypt the users private key
  328. *
  329. * @param string $password
  330. * @param string $cipher
  331. * @param string $uid only used for user keys
  332. * @return string
  333. */
  334. protected function generatePasswordHash(string $password, string $cipher, string $uid = '', int $iterations = 600000): string {
  335. $instanceId = $this->config->getSystemValue('instanceid');
  336. $instanceSecret = $this->config->getSystemValue('secret');
  337. $salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
  338. $keySize = $this->getKeySize($cipher);
  339. return hash_pbkdf2(
  340. 'sha256',
  341. $password,
  342. $salt,
  343. $iterations,
  344. $keySize,
  345. true
  346. );
  347. }
  348. /**
  349. * encrypt private key
  350. *
  351. * @param string $privateKey
  352. * @param string $password
  353. * @param string $uid for regular users, empty for system keys
  354. * @return false|string
  355. */
  356. public function encryptPrivateKey($privateKey, $password, $uid = '') {
  357. $cipher = $this->getCipher();
  358. $hash = $this->generatePasswordHash($password, $cipher, $uid);
  359. $encryptedKey = $this->symmetricEncryptFileContent(
  360. $privateKey,
  361. $hash,
  362. 0,
  363. 0
  364. );
  365. return $encryptedKey;
  366. }
  367. /**
  368. * @param string $privateKey
  369. * @param string $password
  370. * @param string $uid for regular users, empty for system keys
  371. * @return false|string
  372. */
  373. public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
  374. $header = $this->parseHeader($privateKey);
  375. if (isset($header['cipher'])) {
  376. $cipher = $header['cipher'];
  377. } else {
  378. $cipher = $this->getLegacyCipher();
  379. }
  380. if (isset($header['keyFormat'])) {
  381. $keyFormat = $header['keyFormat'];
  382. } else {
  383. $keyFormat = self::LEGACY_KEY_FORMAT;
  384. }
  385. if ($keyFormat === 'hash') {
  386. $password = $this->generatePasswordHash($password, $cipher, $uid, 100000);
  387. } elseif ($keyFormat === 'hash2') {
  388. $password = $this->generatePasswordHash($password, $cipher, $uid, 600000);
  389. }
  390. $binaryEncoding = isset($header['encoding']) && $header['encoding'] === self::BINARY_ENCODING_FORMAT;
  391. // If we found a header we need to remove it from the key we want to decrypt
  392. if (!empty($header)) {
  393. $privateKey = substr($privateKey,
  394. strpos($privateKey,
  395. self::HEADER_END) + strlen(self::HEADER_END));
  396. }
  397. $plainKey = $this->symmetricDecryptFileContent(
  398. $privateKey,
  399. $password,
  400. $cipher,
  401. 0,
  402. 0,
  403. $binaryEncoding
  404. );
  405. if ($this->isValidPrivateKey($plainKey) === false) {
  406. return false;
  407. }
  408. return $plainKey;
  409. }
  410. /**
  411. * check if it is a valid private key
  412. *
  413. * @param string $plainKey
  414. * @return bool
  415. */
  416. protected function isValidPrivateKey($plainKey) {
  417. $res = openssl_get_privatekey($plainKey);
  418. if (is_object($res) && get_class($res) === 'OpenSSLAsymmetricKey') {
  419. $sslInfo = openssl_pkey_get_details($res);
  420. if (isset($sslInfo['key'])) {
  421. return true;
  422. }
  423. }
  424. return false;
  425. }
  426. /**
  427. * @param string $keyFileContents
  428. * @param string $passPhrase
  429. * @param string $cipher
  430. * @param int $version
  431. * @param int|string $position
  432. * @param boolean $binaryEncoding
  433. * @return string
  434. * @throws DecryptionFailedException
  435. */
  436. public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0, bool $binaryEncoding = false) {
  437. if ($keyFileContents == '') {
  438. return '';
  439. }
  440. $catFile = $this->splitMetaData($keyFileContents, $cipher);
  441. if ($catFile['signature'] !== false) {
  442. try {
  443. // First try the new format
  444. $this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
  445. } catch (GenericEncryptionException $e) {
  446. // For compatibility with old files check the version without _
  447. $this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
  448. }
  449. }
  450. return $this->decrypt($catFile['encrypted'],
  451. $catFile['iv'],
  452. $passPhrase,
  453. $cipher,
  454. $binaryEncoding);
  455. }
  456. /**
  457. * check for valid signature
  458. *
  459. * @throws GenericEncryptionException
  460. */
  461. private function checkSignature(string $data, string $passPhrase, string $expectedSignature): void {
  462. $enforceSignature = !$this->config->getSystemValueBool('encryption_skip_signature_check', false);
  463. $signature = $this->createSignature($data, $passPhrase);
  464. $isCorrectHash = hash_equals($expectedSignature, $signature);
  465. if (!$isCorrectHash && $enforceSignature) {
  466. throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
  467. } elseif (!$isCorrectHash && !$enforceSignature) {
  468. $this->logger->info("Signature check skipped", ['app' => 'encryption']);
  469. }
  470. }
  471. /**
  472. * create signature
  473. *
  474. * @param string $data
  475. * @param string $passPhrase
  476. * @return string
  477. */
  478. private function createSignature($data, $passPhrase) {
  479. $passPhrase = hash('sha512', $passPhrase . 'a', true);
  480. return hash_hmac('sha256', $data, $passPhrase);
  481. }
  482. /**
  483. * remove padding
  484. *
  485. * @param string $padded
  486. * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
  487. * @return string|false
  488. */
  489. private function removePadding($padded, $hasSignature = false) {
  490. if ($hasSignature === false && substr($padded, -2) === 'xx') {
  491. return substr($padded, 0, -2);
  492. } elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
  493. return substr($padded, 0, -3);
  494. }
  495. return false;
  496. }
  497. /**
  498. * split meta data from encrypted file
  499. * Note: for now, we assume that the meta data always start with the iv
  500. * followed by the signature, if available
  501. *
  502. * @param string $catFile
  503. * @param string $cipher
  504. * @return array
  505. */
  506. private function splitMetaData($catFile, $cipher) {
  507. if ($this->hasSignature($catFile, $cipher)) {
  508. $catFile = $this->removePadding($catFile, true);
  509. $meta = substr($catFile, -93);
  510. $iv = substr($meta, strlen('00iv00'), 16);
  511. $sig = substr($meta, 22 + strlen('00sig00'));
  512. $encrypted = substr($catFile, 0, -93);
  513. } else {
  514. $catFile = $this->removePadding($catFile);
  515. $meta = substr($catFile, -22);
  516. $iv = substr($meta, -16);
  517. $sig = false;
  518. $encrypted = substr($catFile, 0, -22);
  519. }
  520. return [
  521. 'encrypted' => $encrypted,
  522. 'iv' => $iv,
  523. 'signature' => $sig
  524. ];
  525. }
  526. /**
  527. * check if encrypted block is signed
  528. *
  529. * @param string $catFile
  530. * @param string $cipher
  531. * @return bool
  532. * @throws GenericEncryptionException
  533. */
  534. private function hasSignature($catFile, $cipher) {
  535. $skipSignatureCheck = $this->config->getSystemValueBool('encryption_skip_signature_check', false);
  536. $meta = substr($catFile, -93);
  537. $signaturePosition = strpos($meta, '00sig00');
  538. // If we no longer support the legacy format then everything needs a signature
  539. if (!$skipSignatureCheck && !$this->supportLegacy && $signaturePosition === false) {
  540. throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
  541. }
  542. // Enforce signature for the new 'CTR' ciphers
  543. if (!$skipSignatureCheck && $signaturePosition === false && stripos($cipher, 'ctr') !== false) {
  544. throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
  545. }
  546. return ($signaturePosition !== false);
  547. }
  548. /**
  549. * @param string $encryptedContent
  550. * @param string $iv
  551. * @param string $passPhrase
  552. * @param string $cipher
  553. * @param boolean $binaryEncoding
  554. * @return string
  555. * @throws DecryptionFailedException
  556. */
  557. private function decrypt(string $encryptedContent, string $iv, string $passPhrase = '', string $cipher = self::DEFAULT_CIPHER, bool $binaryEncoding = false): string {
  558. $options = $binaryEncoding === true ? OPENSSL_RAW_DATA : 0;
  559. $plainContent = openssl_decrypt($encryptedContent,
  560. $cipher,
  561. $passPhrase,
  562. $options,
  563. $iv);
  564. if ($plainContent) {
  565. return $plainContent;
  566. } else {
  567. throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
  568. }
  569. }
  570. /**
  571. * @param string $data
  572. * @return array
  573. */
  574. protected function parseHeader($data) {
  575. $result = [];
  576. if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
  577. $endAt = strpos($data, self::HEADER_END);
  578. $header = substr($data, 0, $endAt + strlen(self::HEADER_END));
  579. // +1 not to start with an ':' which would result in empty element at the beginning
  580. $exploded = explode(':',
  581. substr($header, strlen(self::HEADER_START) + 1));
  582. $element = array_shift($exploded);
  583. while ($element !== self::HEADER_END) {
  584. $result[$element] = array_shift($exploded);
  585. $element = array_shift($exploded);
  586. }
  587. }
  588. return $result;
  589. }
  590. /**
  591. * generate initialization vector
  592. *
  593. * @return string
  594. * @throws GenericEncryptionException
  595. */
  596. private function generateIv() {
  597. return random_bytes(16);
  598. }
  599. /**
  600. * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
  601. * as file key
  602. *
  603. * @return string
  604. * @throws \Exception
  605. */
  606. public function generateFileKey() {
  607. return random_bytes(32);
  608. }
  609. /**
  610. * @param string $encKeyFile
  611. * @param string $shareKey
  612. * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $privateKey
  613. * @return string
  614. * @throws MultiKeyDecryptException
  615. */
  616. public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
  617. if (!$encKeyFile) {
  618. throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
  619. }
  620. $plainContent = '';
  621. if ($this->opensslOpen($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) {
  622. return $plainContent;
  623. } else {
  624. throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
  625. }
  626. }
  627. /**
  628. * @param string $plainContent
  629. * @param array $keyFiles
  630. * @return array
  631. * @throws MultiKeyEncryptException
  632. */
  633. public function multiKeyEncrypt($plainContent, array $keyFiles) {
  634. // openssl_seal returns false without errors if plaincontent is empty
  635. // so trigger our own error
  636. if (empty($plainContent)) {
  637. throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
  638. }
  639. // Set empty vars to be set by openssl by reference
  640. $sealed = '';
  641. $shareKeys = [];
  642. $mappedShareKeys = [];
  643. if ($this->opensslSeal($plainContent, $sealed, $shareKeys, $keyFiles, 'RC4')) {
  644. $i = 0;
  645. // Ensure each shareKey is labelled with its corresponding key id
  646. foreach ($keyFiles as $userId => $publicKey) {
  647. $mappedShareKeys[$userId] = $shareKeys[$i];
  648. $i++;
  649. }
  650. return [
  651. 'keys' => $mappedShareKeys,
  652. 'data' => $sealed
  653. ];
  654. } else {
  655. throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
  656. }
  657. }
  658. /**
  659. * returns the value of $useLegacyBase64Encoding
  660. *
  661. * @return bool
  662. */
  663. public function useLegacyBase64Encoding(): bool {
  664. return $this->useLegacyBase64Encoding;
  665. }
  666. /**
  667. * Uses phpseclib RC4 implementation
  668. */
  669. private function rc4Decrypt(string $data, string $secret): string {
  670. $rc4 = new RC4();
  671. /** @psalm-suppress InternalMethod */
  672. $rc4->setKey($secret);
  673. return $rc4->decrypt($data);
  674. }
  675. /**
  676. * Uses phpseclib RC4 implementation
  677. */
  678. private function rc4Encrypt(string $data, string $secret): string {
  679. $rc4 = new RC4();
  680. /** @psalm-suppress InternalMethod */
  681. $rc4->setKey($secret);
  682. return $rc4->encrypt($data);
  683. }
  684. /**
  685. * Custom implementation of openssl_open()
  686. *
  687. * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $private_key
  688. * @throws DecryptionFailedException
  689. */
  690. private function opensslOpen(string $data, string &$output, string $encrypted_key, $private_key, string $cipher_algo): bool {
  691. $result = false;
  692. // check if RC4 is used
  693. if (strcasecmp($cipher_algo, "rc4") === 0) {
  694. // decrypt the intermediate key with RSA
  695. if (openssl_private_decrypt($encrypted_key, $intermediate, $private_key, OPENSSL_PKCS1_PADDING)) {
  696. // decrypt the file key with the intermediate key
  697. // using our own RC4 implementation
  698. $output = $this->rc4Decrypt($data, $intermediate);
  699. $result = (strlen($output) === strlen($data));
  700. }
  701. } else {
  702. throw new DecryptionFailedException('Unsupported cipher '.$cipher_algo);
  703. }
  704. return $result;
  705. }
  706. /**
  707. * Custom implementation of openssl_seal()
  708. *
  709. * @throws EncryptionFailedException
  710. */
  711. private function opensslSeal(string $data, string &$sealed_data, array &$encrypted_keys, array $public_key, string $cipher_algo): int|false {
  712. $result = false;
  713. // check if RC4 is used
  714. if (strcasecmp($cipher_algo, "rc4") === 0) {
  715. // make sure that there is at least one public key to use
  716. if (count($public_key) >= 1) {
  717. // generate the intermediate key
  718. $intermediate = openssl_random_pseudo_bytes(16, $strong_result);
  719. // check if we got strong random data
  720. if ($strong_result) {
  721. // encrypt the file key with the intermediate key
  722. // using our own RC4 implementation
  723. $sealed_data = $this->rc4Encrypt($data, $intermediate);
  724. if (strlen($sealed_data) === strlen($data)) {
  725. // prepare the encrypted keys
  726. $encrypted_keys = [];
  727. // iterate over the public keys and encrypt the intermediate
  728. // for each of them with RSA
  729. foreach ($public_key as $tmp_key) {
  730. if (openssl_public_encrypt($intermediate, $tmp_output, $tmp_key, OPENSSL_PKCS1_PADDING)) {
  731. $encrypted_keys[] = $tmp_output;
  732. }
  733. }
  734. // set the result if everything worked fine
  735. if (count($public_key) === count($encrypted_keys)) {
  736. $result = strlen($sealed_data);
  737. }
  738. }
  739. }
  740. }
  741. } else {
  742. throw new EncryptionFailedException('Unsupported cipher '.$cipher_algo);
  743. }
  744. return $result;
  745. }
  746. }