You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

840 lines
24 KiB

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