util.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. <?php
  2. /**
  3. * ownCloud
  4. *
  5. * @author Sam Tuke, Frank Karlitschek
  6. * @copyright 2012 Sam Tuke samtuke@owncloud.com,
  7. * Frank Karlitschek frank@owncloud.org
  8. *
  9. * This library is free software; you can redistribute it and/or
  10. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  11. * License as published by the Free Software Foundation; either
  12. * version 3 of the License, or any later version.
  13. *
  14. * This library is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public
  20. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. */
  23. // Todo:
  24. // - Crypt/decrypt button in the userinterface
  25. // - Setting if crypto should be on by default
  26. // - Add a setting "Don´t encrypt files larger than xx because of performance
  27. // reasons"
  28. // - Transparent decrypt/encrypt in filesystem.php. Autodetect if a file is
  29. // encrypted (.encrypted extension)
  30. // - Don't use a password directly as encryption key. but a key which is
  31. // stored on the server and encrypted with the user password. -> password
  32. // change faster
  33. // - IMPORTANT! Check if the block lenght of the encrypted data stays the same
  34. namespace OCA\Encryption;
  35. /**
  36. * @brief Class for utilities relating to encrypted file storage system
  37. * @param OC_FilesystemView $view expected to have OC '/' as root path
  38. * @param string $userId ID of the logged in user
  39. * @param int $client indicating status of client side encryption. Currently
  40. * unused, likely to become obsolete shortly
  41. */
  42. class Util {
  43. // Web UI:
  44. //// DONE: files created via web ui are encrypted
  45. //// DONE: file created & encrypted via web ui are readable in web ui
  46. //// DONE: file created & encrypted via web ui are readable via webdav
  47. // WebDAV:
  48. //// DONE: new data filled files added via webdav get encrypted
  49. //// DONE: new data filled files added via webdav are readable via webdav
  50. //// DONE: reading unencrypted files when encryption is enabled works via
  51. //// webdav
  52. //// DONE: files created & encrypted via web ui are readable via webdav
  53. // Legacy support:
  54. //// DONE: add method to check if file is encrypted using new system
  55. //// DONE: add method to check if file is encrypted using old system
  56. //// DONE: add method to fetch legacy key
  57. //// DONE: add method to decrypt legacy encrypted data
  58. // Admin UI:
  59. //// DONE: changing user password also changes encryption passphrase
  60. //// TODO: add support for optional recovery in case of lost passphrase / keys
  61. //// TODO: add admin optional required long passphrase for users
  62. //// TODO: add UI buttons for encrypt / decrypt everything
  63. //// TODO: implement flag system to allow user to specify encryption by folder, subfolder, etc.
  64. // Sharing:
  65. //// TODO: add support for encrypting to multiple public keys
  66. //// TODO: add support for decrypting to multiple private keys
  67. // Integration testing:
  68. //// TODO: test new encryption with versioning
  69. //// TODO: test new encryption with sharing
  70. //// TODO: test new encryption with proxies
  71. private $view; // OC_FilesystemView object for filesystem operations
  72. private $userId; // ID of the currently logged-in user
  73. private $pwd; // User Password
  74. private $client; // Client side encryption mode flag
  75. private $publicKeyDir; // Dir containing all public user keys
  76. private $encryptionDir; // Dir containing user's files_encryption
  77. private $keyfilesPath; // Dir containing user's keyfiles
  78. private $shareKeysPath; // Dir containing env keys for shared files
  79. private $publicKeyPath; // Path to user's public key
  80. private $privateKeyPath; // Path to user's private key
  81. public function __construct( \OC_FilesystemView $view, $userId, $client = false ) {
  82. $this->view = $view;
  83. $this->userId = $userId;
  84. $this->client = $client;
  85. $this->userDir = '/' . $this->userId;
  86. $this->userFilesDir = '/' . $this->userId . '/' . 'files';
  87. $this->publicKeyDir = '/' . 'public-keys';
  88. $this->encryptionDir = '/' . $this->userId . '/' . 'files_encryption';
  89. $this->keyfilesPath = $this->encryptionDir . '/' . 'keyfiles';
  90. $this->shareKeysPath = $this->encryptionDir . '/' . 'share-keys';
  91. $this->publicKeyPath = $this->publicKeyDir . '/' . $this->userId . '.public.key'; // e.g. data/public-keys/admin.public.key
  92. $this->privateKeyPath = $this->encryptionDir . '/' . $this->userId . '.private.key'; // e.g. data/admin/admin.private.key
  93. }
  94. public function ready() {
  95. if(
  96. !$this->view->file_exists( $this->encryptionDir )
  97. or !$this->view->file_exists( $this->keyfilesPath )
  98. or !$this->view->file_exists( $this->shareKeysPath )
  99. or !$this->view->file_exists( $this->publicKeyPath )
  100. or !$this->view->file_exists( $this->privateKeyPath )
  101. ) {
  102. return false;
  103. } else {
  104. return true;
  105. }
  106. }
  107. /**
  108. * @brief Sets up user folders and keys for serverside encryption
  109. * @param $passphrase passphrase to encrypt server-stored private key with
  110. */
  111. public function setupServerSide( $passphrase = null ) {
  112. // Create user dir
  113. if( !$this->view->file_exists( $this->userDir ) ) {
  114. $this->view->mkdir( $this->userDir );
  115. }
  116. // Create user files dir
  117. if( !$this->view->file_exists( $this->userFilesDir ) ) {
  118. $this->view->mkdir( $this->userFilesDir );
  119. }
  120. // Create shared public key directory
  121. if( !$this->view->file_exists( $this->publicKeyDir ) ) {
  122. $this->view->mkdir( $this->publicKeyDir );
  123. }
  124. // Create encryption app directory
  125. if( !$this->view->file_exists( $this->encryptionDir ) ) {
  126. $this->view->mkdir( $this->encryptionDir );
  127. }
  128. // Create mirrored keyfile directory
  129. if( !$this->view->file_exists( $this->keyfilesPath ) ) {
  130. $this->view->mkdir( $this->keyfilesPath );
  131. }
  132. // Create mirrored share env keys directory
  133. if( !$this->view->file_exists( $this->shareKeysPath ) ) {
  134. $this->view->mkdir( $this->shareKeysPath );
  135. }
  136. // Create user keypair
  137. if (
  138. ! $this->view->file_exists( $this->publicKeyPath )
  139. or ! $this->view->file_exists( $this->privateKeyPath )
  140. ) {
  141. // Generate keypair
  142. $keypair = Crypt::createKeypair();
  143. \OC_FileProxy::$enabled = false;
  144. // Save public key
  145. $this->view->file_put_contents( $this->publicKeyPath, $keypair['publicKey'] );
  146. // Encrypt private key with user pwd as passphrase
  147. $encryptedPrivateKey = Crypt::symmetricEncryptFileContent( $keypair['privateKey'], $passphrase );
  148. // Save private key
  149. $this->view->file_put_contents( $this->privateKeyPath, $encryptedPrivateKey );
  150. \OC_FileProxy::$enabled = true;
  151. }
  152. return true;
  153. }
  154. /**
  155. * @brief Find all files and their encryption status within a directory
  156. * @param string $directory The path of the parent directory to search
  157. * @return mixed false if 0 found, array on success. Keys: name, path
  158. * @note $directory needs to be a path relative to OC data dir. e.g.
  159. * /admin/files NOT /backup OR /home/www/oc/data/admin/files
  160. */
  161. public function findFiles( $directory ) {
  162. // Disable proxy - we don't want files to be decrypted before
  163. // we handle them
  164. \OC_FileProxy::$enabled = false;
  165. $found = array( 'plain' => array(), 'encrypted' => array(), 'legacy' => array() );
  166. if (
  167. $this->view->is_dir( $directory )
  168. && $handle = $this->view->opendir( $directory )
  169. ) {
  170. while ( false !== ( $file = readdir( $handle ) ) ) {
  171. if (
  172. $file != "."
  173. && $file != ".."
  174. ) {
  175. $filePath = $directory . '/' . $this->view->getRelativePath( '/' . $file );
  176. $relPath = $this->stripUserFilesPath( $filePath );
  177. // If the path is a directory, search
  178. // its contents
  179. if ( $this->view->is_dir( $filePath ) ) {
  180. $this->findFiles( $filePath );
  181. // If the path is a file, determine
  182. // its encryption status
  183. } elseif ( $this->view->is_file( $filePath ) ) {
  184. // Disable proxies again, some-
  185. // where they got re-enabled :/
  186. \OC_FileProxy::$enabled = false;
  187. $data = $this->view->file_get_contents( $filePath );
  188. // If the file is encrypted
  189. // NOTE: If the userId is
  190. // empty or not set, file will
  191. // detected as plain
  192. // NOTE: This is inefficient;
  193. // scanning every file like this
  194. // will eat server resources :(
  195. if (
  196. Keymanager::getFileKey( $this->view, $this->userId, $file )
  197. && Crypt::isCatfile( $data )
  198. ) {
  199. $found['encrypted'][] = array( 'name' => $file, 'path' => $filePath );
  200. // If the file uses old
  201. // encryption system
  202. } elseif ( Crypt::isLegacyEncryptedContent( $this->view->file_get_contents( $filePath ), $relPath ) ) {
  203. $found['legacy'][] = array( 'name' => $file, 'path' => $filePath );
  204. // If the file is not encrypted
  205. } else {
  206. $found['plain'][] = array( 'name' => $file, 'path' => $filePath );
  207. }
  208. }
  209. }
  210. }
  211. \OC_FileProxy::$enabled = true;
  212. if ( empty( $found ) ) {
  213. return false;
  214. } else {
  215. return $found;
  216. }
  217. }
  218. \OC_FileProxy::$enabled = true;
  219. return false;
  220. }
  221. /**
  222. * @brief Check if a given path identifies an encrypted file
  223. * @return true / false
  224. */
  225. public function isEncryptedPath( $path ) {
  226. // Disable encryption proxy so data retreived is in its
  227. // original form
  228. \OC_FileProxy::$enabled = false;
  229. $data = $this->view->file_get_contents( $path );
  230. \OC_FileProxy::$enabled = true;
  231. return Crypt::isCatfile( $data );
  232. }
  233. /**
  234. * @brief Format a path to be relative to the /user/files/ directory
  235. */
  236. public function stripUserFilesPath( $path ) {
  237. $trimmed = ltrim( $path, '/' );
  238. $split = explode( '/', $trimmed );
  239. $sliced = array_slice( $split, 2 );
  240. $relPath = implode( '/', $sliced );
  241. return $relPath;
  242. }
  243. /**
  244. * @brief Encrypt all files in a directory
  245. * @param string $publicKey the public key to encrypt files with
  246. * @param string $dirPath the directory whose files will be encrypted
  247. * @note Encryption is recursive
  248. */
  249. public function encryptAll( $publicKey, $dirPath, $legacyPassphrase = null, $newPassphrase = null ) {
  250. if ( $found = $this->findFiles( $dirPath ) ) {
  251. // Disable proxy to prevent file being encrypted twice
  252. \OC_FileProxy::$enabled = false;
  253. // Encrypt unencrypted files
  254. foreach ( $found['plain'] as $plainFile ) {
  255. // Fetch data from file
  256. $plainData = $this->view->file_get_contents( $plainFile['path'] );
  257. // Encrypt data, generate catfile
  258. $encrypted = Crypt::keyEncryptKeyfile( $plainData, $publicKey );
  259. $relPath = $this->stripUserFilesPath( $plainFile['path'] );
  260. // Save keyfile
  261. Keymanager::setFileKey( $this->view, $relPath, $this->userId, $encrypted['key'] );
  262. // Overwrite the existing file with the encrypted one
  263. $this->view->file_put_contents( $plainFile['path'], $encrypted['data'] );
  264. $size = strlen( $encrypted['data'] );
  265. // Add the file to the cache
  266. \OC\Files\Filesystem::putFileInfo( $plainFile['path'], array( 'encrypted'=>true, 'size' => $size ), '' );
  267. }
  268. // Encrypt legacy encrypted files
  269. if (
  270. ! empty( $legacyPassphrase )
  271. && ! empty( $newPassphrase )
  272. ) {
  273. foreach ( $found['legacy'] as $legacyFile ) {
  274. // Fetch data from file
  275. $legacyData = $this->view->file_get_contents( $legacyFile['path'] );
  276. // Recrypt data, generate catfile
  277. $recrypted = Crypt::legacyKeyRecryptKeyfile( $legacyData, $legacyPassphrase, $publicKey, $newPassphrase );
  278. $relPath = $this->stripUserFilesPath( $legacyFile['path'] );
  279. // Save keyfile
  280. Keymanager::setFileKey( $this->view, $relPath, $this->userId, $recrypted['key'] );
  281. // Overwrite the existing file with the encrypted one
  282. $this->view->file_put_contents( $legacyFile['path'], $recrypted['data'] );
  283. $size = strlen( $recrypted['data'] );
  284. // Add the file to the cache
  285. \OC\Files\Filesystem::putFileInfo( $legacyFile['path'], array( 'encrypted'=>true, 'size' => $size ), '' );
  286. }
  287. }
  288. \OC_FileProxy::$enabled = true;
  289. // If files were found, return true
  290. return true;
  291. } else {
  292. // If no files were found, return false
  293. return false;
  294. }
  295. }
  296. /**
  297. * @brief Return important encryption related paths
  298. * @param string $pathName Name of the directory to return the path of
  299. * @return string path
  300. */
  301. public function getPath( $pathName ) {
  302. switch ( $pathName ) {
  303. case 'publicKeyDir':
  304. return $this->publicKeyDir;
  305. break;
  306. case 'encryptionDir':
  307. return $this->encryptionDir;
  308. break;
  309. case 'keyfilesPath':
  310. return $this->keyfilesPath;
  311. break;
  312. case 'publicKeyPath':
  313. return $this->publicKeyPath;
  314. break;
  315. case 'privateKeyPath':
  316. return $this->privateKeyPath;
  317. break;
  318. }
  319. }
  320. }