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 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. <?php
  2. /**
  3. * @author Björn Schießle <schiessle@owncloud.com>
  4. * @author Florin Peter <github@florin-peter.de>
  5. * @author Joas Schilling <nickvergessen@owncloud.com>
  6. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  7. * @author Morris Jobke <hey@morrisjobke.de>
  8. * @author Owen Winkler <a_github@midnightcircus.com>
  9. * @author Robin McCorkell <rmccorkell@karoshi.org.uk>
  10. * @author Sam Tuke <mail@samtuke.com>
  11. * @author Scott Arciszewski <scott@arciszewski.me>
  12. * @author Thomas Müller <thomas.mueller@tmit.eu>
  13. *
  14. * @copyright Copyright (c) 2015, ownCloud, Inc.
  15. * @license AGPL-3.0
  16. *
  17. * This code is free software: you can redistribute it and/or modify
  18. * it under the terms of the GNU Affero General Public License, version 3,
  19. * as published by the Free Software Foundation.
  20. *
  21. * This program is distributed in the hope that it will be useful,
  22. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  23. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  24. * GNU Affero General Public License for more details.
  25. *
  26. * You should have received a copy of the GNU Affero General Public License, version 3,
  27. * along with this program. If not, see <http://www.gnu.org/licenses/>
  28. *
  29. */
  30. namespace OCA\Files_Encryption;
  31. /**
  32. * Class for common cryptography functionality
  33. */
  34. class Crypt {
  35. const ENCRYPTION_UNKNOWN_ERROR = -1;
  36. const ENCRYPTION_NOT_INITIALIZED_ERROR = 1;
  37. const ENCRYPTION_PRIVATE_KEY_NOT_VALID_ERROR = 2;
  38. const ENCRYPTION_NO_SHARE_KEY_FOUND = 3;
  39. const BLOCKSIZE = 8192; // block size will always be 8192 for a PHP stream https://bugs.php.net/bug.php?id=21641
  40. const DEFAULT_CIPHER = 'AES-256-CFB';
  41. const HEADERSTART = 'HBEGIN';
  42. const HEADEREND = 'HEND';
  43. /**
  44. * return encryption mode client or server side encryption
  45. * @param string $user name (use system wide setting if name=null)
  46. * @return string 'client' or 'server'
  47. * @note at the moment we only support server side encryption
  48. */
  49. public static function mode($user = null) {
  50. return 'server';
  51. }
  52. /**
  53. * Create a new encryption keypair
  54. * @return array publicKey, privatekey
  55. */
  56. public static function createKeypair() {
  57. $return = false;
  58. $res = Helper::getOpenSSLPkey();
  59. if ($res === false) {
  60. \OCP\Util::writeLog('Encryption library', 'couldn\'t generate users key-pair for ' . \OCP\User::getUser(), \OCP\Util::ERROR);
  61. while ($msg = openssl_error_string()) {
  62. \OCP\Util::writeLog('Encryption library', 'openssl_pkey_new() fails: ' . $msg, \OCP\Util::ERROR);
  63. }
  64. } elseif (openssl_pkey_export($res, $privateKey, null, Helper::getOpenSSLConfig())) {
  65. // Get public key
  66. $keyDetails = openssl_pkey_get_details($res);
  67. $publicKey = $keyDetails['key'];
  68. $return = array(
  69. 'publicKey' => $publicKey,
  70. 'privateKey' => $privateKey
  71. );
  72. } else {
  73. \OCP\Util::writeLog('Encryption library', 'couldn\'t export users private key, please check your servers openSSL configuration.' . \OCP\User::getUser(), \OCP\Util::ERROR);
  74. while($errMsg = openssl_error_string()) {
  75. \OCP\Util::writeLog('Encryption library', $errMsg, \OCP\Util::ERROR);
  76. }
  77. }
  78. return $return;
  79. }
  80. /**
  81. * Add arbitrary padding to encrypted data
  82. * @param string $data data to be padded
  83. * @return string padded data
  84. * @note In order to end up with data exactly 8192 bytes long we must
  85. * add two letters. It is impossible to achieve exactly 8192 length
  86. * blocks with encryption alone, hence padding is added to achieve the
  87. * required length.
  88. */
  89. private static function addPadding($data) {
  90. $padded = $data . 'xx';
  91. return $padded;
  92. }
  93. /**
  94. * Remove arbitrary padding to encrypted data
  95. * @param string $padded padded data to remove padding from
  96. * @return string unpadded data on success, false on error
  97. */
  98. private static function removePadding($padded) {
  99. if (substr($padded, -2) === 'xx') {
  100. $data = substr($padded, 0, -2);
  101. return $data;
  102. } else {
  103. // TODO: log the fact that unpadded data was submitted for removal of padding
  104. return false;
  105. }
  106. }
  107. /**
  108. * Check if a file's contents contains an IV and is symmetrically encrypted
  109. * @param string $content
  110. * @return boolean
  111. * @note see also \OCA\Files_Encryption\Util->isEncryptedPath()
  112. */
  113. public static function isCatfileContent($content) {
  114. if (!$content) {
  115. return false;
  116. }
  117. $noPadding = self::removePadding($content);
  118. // Fetch encryption metadata from end of file
  119. $meta = substr($noPadding, -22);
  120. // Fetch identifier from start of metadata
  121. $identifier = substr($meta, 0, 6);
  122. if ($identifier === '00iv00') {
  123. return true;
  124. } else {
  125. return false;
  126. }
  127. }
  128. /**
  129. * Check if a file is encrypted according to database file cache
  130. * @param string $path
  131. * @return bool
  132. */
  133. public static function isEncryptedMeta($path) {
  134. // TODO: Use DI to get \OC\Files\Filesystem out of here
  135. // Fetch all file metadata from DB
  136. $metadata = \OC\Files\Filesystem::getFileInfo($path);
  137. // Return encryption status
  138. return isset($metadata['encrypted']) && ( bool )$metadata['encrypted'];
  139. }
  140. /**
  141. * Symmetrically encrypt a string
  142. * @param string $plainContent
  143. * @param string $iv
  144. * @param string $passphrase
  145. * @param string $cypher used for encryption, currently we support AES-128-CFB and AES-256-CFB
  146. * @return string encrypted file content
  147. * @throws \OCA\Files_Encryption\Exception\EncryptionException
  148. */
  149. private static function encrypt($plainContent, $iv, $passphrase = '', $cipher = Crypt::DEFAULT_CIPHER) {
  150. $encryptedContent = openssl_encrypt($plainContent, $cipher, $passphrase, false, $iv);
  151. if (!$encryptedContent) {
  152. $error = "Encryption (symmetric) of content failed: " . openssl_error_string();
  153. \OCP\Util::writeLog('Encryption library', $error, \OCP\Util::ERROR);
  154. throw new Exception\EncryptionException($error, Exception\EncryptionException::ENCRYPTION_FAILED);
  155. }
  156. return $encryptedContent;
  157. }
  158. /**
  159. * Symmetrically decrypt a string
  160. * @param string $encryptedContent
  161. * @param string $iv
  162. * @param string $passphrase
  163. * @param string $cipher cipher user for decryption, currently we support aes128 and aes256
  164. * @throws \Exception
  165. * @return string decrypted file content
  166. */
  167. private static function decrypt($encryptedContent, $iv, $passphrase, $cipher = Crypt::DEFAULT_CIPHER) {
  168. $plainContent = openssl_decrypt($encryptedContent, $cipher, $passphrase, false, $iv);
  169. if ($plainContent) {
  170. return $plainContent;
  171. } else {
  172. throw new \Exception('Encryption library: Decryption (symmetric) of content failed');
  173. }
  174. }
  175. /**
  176. * Concatenate encrypted data with its IV and padding
  177. * @param string $content content to be concatenated
  178. * @param string $iv IV to be concatenated
  179. * @return string concatenated content
  180. */
  181. private static function concatIv($content, $iv) {
  182. $combined = $content . '00iv00' . $iv;
  183. return $combined;
  184. }
  185. /**
  186. * Split concatenated data and IV into respective parts
  187. * @param string $catFile concatenated data to be split
  188. * @return array keys: encrypted, iv
  189. */
  190. private static function splitIv($catFile) {
  191. // Fetch encryption metadata from end of file
  192. $meta = substr($catFile, -22);
  193. // Fetch IV from end of file
  194. $iv = substr($meta, -16);
  195. // Remove IV and IV identifier text to expose encrypted content
  196. $encrypted = substr($catFile, 0, -22);
  197. $split = array(
  198. 'encrypted' => $encrypted,
  199. 'iv' => $iv
  200. );
  201. return $split;
  202. }
  203. /**
  204. * Symmetrically encrypts a string and returns keyfile content
  205. * @param string $plainContent content to be encrypted in keyfile
  206. * @param string $passphrase
  207. * @param string $cypher used for encryption, currently we support AES-128-CFB and AES-256-CFB
  208. * @return false|string encrypted content combined with IV
  209. * @note IV need not be specified, as it will be stored in the returned keyfile
  210. * and remain accessible therein.
  211. */
  212. public static function symmetricEncryptFileContent($plainContent, $passphrase = '', $cipher = Crypt::DEFAULT_CIPHER) {
  213. if (!$plainContent) {
  214. \OCP\Util::writeLog('Encryption library', 'symmetrically encryption failed, no content given.', \OCP\Util::ERROR);
  215. return false;
  216. }
  217. $iv = self::generateIv();
  218. try {
  219. $encryptedContent = self::encrypt($plainContent, $iv, $passphrase, $cipher);
  220. // Combine content to encrypt with IV identifier and actual IV
  221. $catfile = self::concatIv($encryptedContent, $iv);
  222. $padded = self::addPadding($catfile);
  223. return $padded;
  224. } catch (Exception\EncryptionException $e) {
  225. $message = 'Could not encrypt file content (code: ' . $e->getCode() . '): ';
  226. \OCP\Util::writeLog('files_encryption', $message . $e->getMessage(), \OCP\Util::ERROR);
  227. return false;
  228. }
  229. }
  230. /**
  231. * Symmetrically decrypts keyfile content
  232. * @param string $keyfileContent
  233. * @param string $passphrase
  234. * @param string $cipher cipher used for decryption, currently aes128 and aes256 is supported.
  235. * @throws \Exception
  236. * @return string|false
  237. * @internal param string $source
  238. * @internal param string $target
  239. * @internal param string $key the decryption key
  240. * @return string decrypted content
  241. *
  242. * This function decrypts a file
  243. */
  244. public static function symmetricDecryptFileContent($keyfileContent, $passphrase = '', $cipher = Crypt::DEFAULT_CIPHER) {
  245. if (!$keyfileContent) {
  246. throw new \Exception('Encryption library: no data provided for decryption');
  247. }
  248. // Remove padding
  249. $noPadding = self::removePadding($keyfileContent);
  250. // Split into enc data and catfile
  251. $catfile = self::splitIv($noPadding);
  252. if ($plainContent = self::decrypt($catfile['encrypted'], $catfile['iv'], $passphrase, $cipher)) {
  253. return $plainContent;
  254. } else {
  255. return false;
  256. }
  257. }
  258. /**
  259. * Decrypt private key and check if the result is a valid keyfile
  260. *
  261. * @param string $encryptedKey encrypted keyfile
  262. * @param string $passphrase to decrypt keyfile
  263. * @return string|false encrypted private key or false
  264. *
  265. * This function decrypts a file
  266. */
  267. public static function decryptPrivateKey($encryptedKey, $passphrase) {
  268. $header = self::parseHeader($encryptedKey);
  269. $cipher = self::getCipher($header);
  270. // if we found a header we need to remove it from the key we want to decrypt
  271. if (!empty($header)) {
  272. $encryptedKey = substr($encryptedKey, strpos($encryptedKey, self::HEADEREND) + strlen(self::HEADEREND));
  273. }
  274. $plainKey = self::symmetricDecryptFileContent($encryptedKey, $passphrase, $cipher);
  275. // check if this a valid private key
  276. $res = openssl_pkey_get_private($plainKey);
  277. if (is_resource($res)) {
  278. $sslInfo = openssl_pkey_get_details($res);
  279. if (!isset($sslInfo['key'])) {
  280. $plainKey = false;
  281. }
  282. } else {
  283. $plainKey = false;
  284. }
  285. return $plainKey;
  286. }
  287. /**
  288. * Create asymmetrically encrypted keyfile content using a generated key
  289. * @param string $plainContent content to be encrypted
  290. * @param array $publicKeys array keys must be the userId of corresponding user
  291. * @return array keys: keys (array, key = userId), data
  292. * @throws \OCA\Files_Encryption\Exception\MultiKeyEncryptException if encryption failed
  293. * @note symmetricDecryptFileContent() can decrypt files created using this method
  294. */
  295. public static function multiKeyEncrypt($plainContent, array $publicKeys) {
  296. // openssl_seal returns false without errors if $plainContent
  297. // is empty, so trigger our own error
  298. if (empty($plainContent)) {
  299. throw new Exception\MultiKeyEncryptException('Cannot multiKeyEncrypt empty plain content', Exception\MultiKeyEncryptException::EMPTY_DATA);
  300. }
  301. // Set empty vars to be set by openssl by reference
  302. $sealed = '';
  303. $shareKeys = array();
  304. $mappedShareKeys = array();
  305. if (openssl_seal($plainContent, $sealed, $shareKeys, $publicKeys)) {
  306. $i = 0;
  307. // Ensure each shareKey is labelled with its
  308. // corresponding userId
  309. foreach ($publicKeys as $userId => $publicKey) {
  310. $mappedShareKeys[$userId] = $shareKeys[$i];
  311. $i++;
  312. }
  313. return array(
  314. 'keys' => $mappedShareKeys,
  315. 'data' => $sealed
  316. );
  317. } else {
  318. throw new Exception\MultiKeyEncryptException('multi key encryption failed: ' . openssl_error_string(),
  319. Exception\MultiKeyEncryptException::OPENSSL_SEAL_FAILED);
  320. }
  321. }
  322. /**
  323. * Asymmetrically encrypt a file using multiple public keys
  324. * @param string $encryptedContent
  325. * @param string $shareKey
  326. * @param mixed $privateKey
  327. * @throws \OCA\Files_Encryption\Exception\MultiKeyDecryptException if decryption failed
  328. * @internal param string $plainContent contains decrypted content
  329. * @return string $plainContent decrypted string
  330. * @note symmetricDecryptFileContent() can be used to decrypt files created using this method
  331. *
  332. * This function decrypts a file
  333. */
  334. public static function multiKeyDecrypt($encryptedContent, $shareKey, $privateKey) {
  335. if (!$encryptedContent) {
  336. throw new Exception\MultiKeyDecryptException('Cannot mutliKeyDecrypt empty plain content',
  337. Exception\MultiKeyDecryptException::EMPTY_DATA);
  338. }
  339. if (openssl_open($encryptedContent, $plainContent, $shareKey, $privateKey)) {
  340. return $plainContent;
  341. } else {
  342. throw new Exception\MultiKeyDecryptException('multiKeyDecrypt with share-key' . $shareKey . 'failed: ' . openssl_error_string(),
  343. Exception\MultiKeyDecryptException::OPENSSL_OPEN_FAILED);
  344. }
  345. }
  346. /**
  347. * Generates a pseudo random initialisation vector
  348. * @return String $iv generated IV
  349. */
  350. private static function generateIv() {
  351. if ($random = openssl_random_pseudo_bytes(12, $strong)) {
  352. if (!$strong) {
  353. // If OpenSSL indicates randomness is insecure, log error
  354. \OCP\Util::writeLog('Encryption library', 'Insecure symmetric key was generated using openssl_random_pseudo_bytes()', \OCP\Util::WARN);
  355. }
  356. // We encode the iv purely for string manipulation
  357. // purposes - it gets decoded before use
  358. $iv = base64_encode($random);
  359. return $iv;
  360. } else {
  361. throw new \Exception('Generating IV failed');
  362. }
  363. }
  364. /**
  365. * Generate a pseudo random 256-bit ASCII key, used as file key
  366. * @return string|false Generated key
  367. */
  368. public static function generateKey() {
  369. // Generate key
  370. if ($key = base64_encode(openssl_random_pseudo_bytes(32, $strong))) {
  371. if (!$strong) {
  372. // If OpenSSL indicates randomness is insecure, log error
  373. throw new \Exception('Encryption library, Insecure symmetric key was generated using openssl_random_pseudo_bytes()');
  374. }
  375. return $key;
  376. } else {
  377. return false;
  378. }
  379. }
  380. /**
  381. * read header into array
  382. *
  383. * @param string $data
  384. * @return array
  385. */
  386. public static function parseHeader($data) {
  387. $result = array();
  388. if (substr($data, 0, strlen(self::HEADERSTART)) === self::HEADERSTART) {
  389. $endAt = strpos($data, self::HEADEREND);
  390. $header = substr($data, 0, $endAt + strlen(self::HEADEREND));
  391. // +1 to not start with an ':' which would result in empty element at the beginning
  392. $exploded = explode(':', substr($header, strlen(self::HEADERSTART)+1));
  393. $element = array_shift($exploded);
  394. while ($element !== self::HEADEREND) {
  395. $result[$element] = array_shift($exploded);
  396. $element = array_shift($exploded);
  397. }
  398. }
  399. return $result;
  400. }
  401. /**
  402. * check if data block is the header
  403. *
  404. * @param string $data
  405. * @return boolean
  406. */
  407. public static function isHeader($data) {
  408. if (substr($data, 0, strlen(self::HEADERSTART)) === self::HEADERSTART) {
  409. return true;
  410. }
  411. return false;
  412. }
  413. /**
  414. * get chiper from header
  415. *
  416. * @param array $header
  417. * @throws \OCA\Files_Encryption\Exception\EncryptionException
  418. */
  419. public static function getCipher($header) {
  420. $cipher = isset($header['cipher']) ? $header['cipher'] : 'AES-128-CFB';
  421. if ($cipher !== 'AES-256-CFB' && $cipher !== 'AES-128-CFB') {
  422. throw new Exception\EncryptionException('file header broken, no supported cipher defined',
  423. Exception\EncryptionException::UNKNOWN_CIPHER);
  424. }
  425. return $cipher;
  426. }
  427. /**
  428. * generate header for encrypted file
  429. */
  430. public static function generateHeader() {
  431. $cipher = Helper::getCipher();
  432. $header = self::HEADERSTART . ':cipher:' . $cipher . ':' . self::HEADEREND;
  433. return $header;
  434. }
  435. }