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.

crypt.php 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. <?php
  2. /**
  3. * @author Björn Schießle <schiessle@owncloud.com>
  4. * @author Clark Tomlinson <fallen013@gmail.com>
  5. * @author Lukas Reschke <lukas@owncloud.com>
  6. * @author Morris Jobke <hey@morrisjobke.de>
  7. * @author Thomas Müller <thomas.mueller@tmit.eu>
  8. *
  9. * @copyright Copyright (c) 2015, ownCloud, Inc.
  10. * @license AGPL-3.0
  11. *
  12. * This code is free software: you can redistribute it and/or modify
  13. * it under the terms of the GNU Affero General Public License, version 3,
  14. * as published by the Free Software Foundation.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU Affero General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Affero General Public License, version 3,
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>
  23. *
  24. */
  25. namespace OCA\Encryption\Crypto;
  26. use OC\Encryption\Exceptions\DecryptionFailedException;
  27. use OC\Encryption\Exceptions\EncryptionFailedException;
  28. use OCA\Encryption\Exceptions\MultiKeyDecryptException;
  29. use OCA\Encryption\Exceptions\MultiKeyEncryptException;
  30. use OCP\Encryption\Exceptions\GenericEncryptionException;
  31. use OCP\IConfig;
  32. use OCP\ILogger;
  33. use OCP\IUser;
  34. use OCP\IUserSession;
  35. class Crypt {
  36. const DEFAULT_CIPHER = 'AES-256-CFB';
  37. // default cipher from old ownCloud versions
  38. const LEGACY_CIPHER = 'AES-128-CFB';
  39. const HEADER_START = 'HBEGIN';
  40. const HEADER_END = 'HEND';
  41. /**
  42. * @var ILogger
  43. */
  44. private $logger;
  45. /**
  46. * @var IUser
  47. */
  48. private $user;
  49. /**
  50. * @var IConfig
  51. */
  52. private $config;
  53. /**
  54. * @param ILogger $logger
  55. * @param IUserSession $userSession
  56. * @param IConfig $config
  57. */
  58. public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config) {
  59. $this->logger = $logger;
  60. $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser() : false;
  61. $this->config = $config;
  62. }
  63. /**
  64. * create new private/public key-pair for user
  65. *
  66. * @return array|bool
  67. */
  68. public function createKeyPair() {
  69. $log = $this->logger;
  70. $res = $this->getOpenSSLPKey();
  71. if (!$res) {
  72. $log->error("Encryption Library couldn't generate users key-pair for {$this->user->getUID()}",
  73. ['app' => 'encryption']);
  74. if (openssl_error_string()) {
  75. $log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
  76. ['app' => 'encryption']);
  77. }
  78. } elseif (openssl_pkey_export($res,
  79. $privateKey,
  80. null,
  81. $this->getOpenSSLConfig())) {
  82. $keyDetails = openssl_pkey_get_details($res);
  83. $publicKey = $keyDetails['key'];
  84. return [
  85. 'publicKey' => $publicKey,
  86. 'privateKey' => $privateKey
  87. ];
  88. }
  89. $log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user->getUID(),
  90. ['app' => 'encryption']);
  91. if (openssl_error_string()) {
  92. $log->error('Encryption Library:' . openssl_error_string(),
  93. ['app' => 'encryption']);
  94. }
  95. return false;
  96. }
  97. /**
  98. * Generates a new private key
  99. *
  100. * @return resource
  101. */
  102. public function getOpenSSLPKey() {
  103. $config = $this->getOpenSSLConfig();
  104. return openssl_pkey_new($config);
  105. }
  106. /**
  107. * get openSSL Config
  108. *
  109. * @return array
  110. */
  111. private function getOpenSSLConfig() {
  112. $config = ['private_key_bits' => 4096];
  113. $config = array_merge(
  114. $config,
  115. $this->config->getSystemValue('openssl', [])
  116. );
  117. return $config;
  118. }
  119. /**
  120. * @param string $plainContent
  121. * @param string $passPhrase
  122. * @return bool|string
  123. * @throws GenericEncryptionException
  124. */
  125. public function symmetricEncryptFileContent($plainContent, $passPhrase) {
  126. if (!$plainContent) {
  127. $this->logger->error('Encryption Library, symmetrical encryption failed no content given',
  128. ['app' => 'encryption']);
  129. return false;
  130. }
  131. $iv = $this->generateIv();
  132. $encryptedContent = $this->encrypt($plainContent,
  133. $iv,
  134. $passPhrase,
  135. $this->getCipher());
  136. // combine content to encrypt the IV identifier and actual IV
  137. $catFile = $this->concatIV($encryptedContent, $iv);
  138. $padded = $this->addPadding($catFile);
  139. return $padded;
  140. }
  141. /**
  142. * generate header for encrypted file
  143. */
  144. public function generateHeader() {
  145. $cipher = $this->getCipher();
  146. $header = self::HEADER_START . ':cipher:' . $cipher . ':' . self::HEADER_END;
  147. return $header;
  148. }
  149. /**
  150. * @param string $plainContent
  151. * @param string $iv
  152. * @param string $passPhrase
  153. * @param string $cipher
  154. * @return string
  155. * @throws EncryptionFailedException
  156. */
  157. private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
  158. $encryptedContent = openssl_encrypt($plainContent,
  159. $cipher,
  160. $passPhrase,
  161. false,
  162. $iv);
  163. if (!$encryptedContent) {
  164. $error = 'Encryption (symmetric) of content failed';
  165. $this->logger->error($error . openssl_error_string(),
  166. ['app' => 'encryption']);
  167. throw new EncryptionFailedException($error);
  168. }
  169. return $encryptedContent;
  170. }
  171. /**
  172. * return Cipher either from config.php or the default cipher defined in
  173. * this class
  174. *
  175. * @return string
  176. */
  177. public function getCipher() {
  178. $cipher = $this->config->getSystemValue('cipher', self::DEFAULT_CIPHER);
  179. if ($cipher !== 'AES-256-CFB' && $cipher !== 'AES-128-CFB') {
  180. $this->logger->warning('Wrong cipher defined in config.php only AES-128-CFB and AES-256-CFB are supported. Fall back' . self::DEFAULT_CIPHER,
  181. ['app' => 'encryption']);
  182. $cipher = self::DEFAULT_CIPHER;
  183. }
  184. return $cipher;
  185. }
  186. /**
  187. * get legacy cipher
  188. *
  189. * @return string
  190. */
  191. public function getLegacyCipher() {
  192. return self::LEGACY_CIPHER;
  193. }
  194. /**
  195. * @param string $encryptedContent
  196. * @param string $iv
  197. * @return string
  198. */
  199. private function concatIV($encryptedContent, $iv) {
  200. return $encryptedContent . '00iv00' . $iv;
  201. }
  202. /**
  203. * @param $data
  204. * @return string
  205. */
  206. private function addPadding($data) {
  207. return $data . 'xx';
  208. }
  209. /**
  210. * @param string $privateKey
  211. * @param string $password
  212. * @return bool|string
  213. */
  214. public function decryptPrivateKey($privateKey, $password = '') {
  215. $header = $this->parseHeader($privateKey);
  216. if (isset($header['cipher'])) {
  217. $cipher = $header['cipher'];
  218. } else {
  219. $cipher = self::LEGACY_CIPHER;
  220. }
  221. // If we found a header we need to remove it from the key we want to decrypt
  222. if (!empty($header)) {
  223. $privateKey = substr($privateKey,
  224. strpos($privateKey,
  225. self::HEADER_END) + strlen(self::HEADER_END));
  226. }
  227. $plainKey = $this->symmetricDecryptFileContent($privateKey,
  228. $password,
  229. $cipher);
  230. // Check if this is a valid private key
  231. $res = openssl_get_privatekey($plainKey);
  232. if (is_resource($res)) {
  233. $sslInfo = openssl_pkey_get_details($res);
  234. if (!isset($sslInfo['key'])) {
  235. return false;
  236. }
  237. } else {
  238. return false;
  239. }
  240. return $plainKey;
  241. }
  242. /**
  243. * @param $keyFileContents
  244. * @param string $passPhrase
  245. * @param string $cipher
  246. * @return string
  247. * @throws DecryptionFailedException
  248. */
  249. public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER) {
  250. // Remove Padding
  251. $noPadding = $this->removePadding($keyFileContents);
  252. $catFile = $this->splitIv($noPadding);
  253. return $this->decrypt($catFile['encrypted'],
  254. $catFile['iv'],
  255. $passPhrase,
  256. $cipher);
  257. }
  258. /**
  259. * remove padding
  260. *
  261. * @param $padded
  262. * @return bool|string
  263. */
  264. private function removePadding($padded) {
  265. if (substr($padded, -2) === 'xx') {
  266. return substr($padded, 0, -2);
  267. }
  268. return false;
  269. }
  270. /**
  271. * split iv from encrypted content
  272. *
  273. * @param $catFile
  274. * @return array
  275. */
  276. private function splitIv($catFile) {
  277. // Fetch encryption metadata from end of file
  278. $meta = substr($catFile, -22);
  279. // Fetch IV from end of file
  280. $iv = substr($meta, -16);
  281. // Remove IV and IV Identifier text to expose encrypted content
  282. $encrypted = substr($catFile, 0, -22);
  283. return [
  284. 'encrypted' => $encrypted,
  285. 'iv' => $iv
  286. ];
  287. }
  288. /**
  289. * @param $encryptedContent
  290. * @param $iv
  291. * @param string $passPhrase
  292. * @param string $cipher
  293. * @return string
  294. * @throws DecryptionFailedException
  295. */
  296. private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
  297. $plainContent = openssl_decrypt($encryptedContent,
  298. $cipher,
  299. $passPhrase,
  300. false,
  301. $iv);
  302. if ($plainContent) {
  303. return $plainContent;
  304. } else {
  305. throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
  306. }
  307. }
  308. /**
  309. * @param $data
  310. * @return array
  311. */
  312. private function parseHeader($data) {
  313. $result = [];
  314. if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
  315. $endAt = strpos($data, self::HEADER_END);
  316. $header = substr($data, 0, $endAt + strlen(self::HEADER_END));
  317. // +1 not to start with an ':' which would result in empty element at the beginning
  318. $exploded = explode(':',
  319. substr($header, strlen(self::HEADER_START) + 1));
  320. $element = array_shift($exploded);
  321. while ($element != self::HEADER_END) {
  322. $result[$element] = array_shift($exploded);
  323. $element = array_shift($exploded);
  324. }
  325. }
  326. return $result;
  327. }
  328. /**
  329. * generate initialization vector
  330. *
  331. * @return string
  332. * @throws GenericEncryptionException
  333. */
  334. private function generateIv() {
  335. $random = openssl_random_pseudo_bytes(12, $strong);
  336. if ($random) {
  337. if (!$strong) {
  338. // If OpenSSL indicates randomness is insecure log error
  339. $this->logger->error('Encryption Library: Insecure symmetric key was generated using openssl_random_psudo_bytes()',
  340. ['app' => 'encryption']);
  341. }
  342. /*
  343. * We encode the iv purely for string manipulation
  344. * purposes -it gets decoded before use
  345. */
  346. return base64_encode($random);
  347. }
  348. // If we ever get here we've failed anyway no need for an else
  349. throw new GenericEncryptionException('Generating IV Failed');
  350. }
  351. /**
  352. * Generate a cryptographically secure pseudo-random base64 encoded 256-bit
  353. * ASCII key, used as file key
  354. *
  355. * @return string
  356. * @throws \Exception
  357. */
  358. public function generateFileKey() {
  359. // Generate key
  360. $key = base64_encode(openssl_random_pseudo_bytes(32, $strong));
  361. if (!$key || !$strong) {
  362. // If OpenSSL indicates randomness is insecure, log error
  363. throw new \Exception('Encryption library, Insecure symmetric key was generated using openssl_random_pseudo_bytes()');
  364. }
  365. return $key;
  366. }
  367. /**
  368. * @param $encKeyFile
  369. * @param $shareKey
  370. * @param $privateKey
  371. * @return mixed
  372. * @throws MultiKeyDecryptException
  373. */
  374. public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
  375. if (!$encKeyFile) {
  376. throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
  377. }
  378. if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) {
  379. return $plainContent;
  380. } else {
  381. throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
  382. }
  383. }
  384. /**
  385. * @param string $plainContent
  386. * @param array $keyFiles
  387. * @return array
  388. * @throws MultiKeyEncryptException
  389. */
  390. public function multiKeyEncrypt($plainContent, array $keyFiles) {
  391. // openssl_seal returns false without errors if plaincontent is empty
  392. // so trigger our own error
  393. if (empty($plainContent)) {
  394. throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
  395. }
  396. // Set empty vars to be set by openssl by reference
  397. $sealed = '';
  398. $shareKeys = [];
  399. $mappedShareKeys = [];
  400. if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles)) {
  401. $i = 0;
  402. // Ensure each shareKey is labelled with its corresponding key id
  403. foreach ($keyFiles as $userId => $publicKey) {
  404. $mappedShareKeys[$userId] = $shareKeys[$i];
  405. $i++;
  406. }
  407. return [
  408. 'keys' => $mappedShareKeys,
  409. 'data' => $sealed
  410. ];
  411. } else {
  412. throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
  413. }
  414. }
  415. }