1
0

Crypt.php 23 KB

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