Crypt.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
  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 phpseclib\Crypt\RC4;
  44. use Psr\Log\LoggerInterface;
  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. }