diff options
author | Florin Peter <github@florin-peter.de> | 2013-05-24 20:54:13 +0200 |
---|---|---|
committer | Florin Peter <github@florin-peter.de> | 2013-05-24 20:54:13 +0200 |
commit | 946e9ccc0ade60bb9ff34ace9e94e85cce6af96c (patch) | |
tree | 4770fb73b25d6eb50caa2149df8fb06dd8928a45 /apps/files_encryption/lib | |
parent | 5076c0d392f6eb17e368a9382cf5b0abe7408889 (diff) | |
parent | ae9adcaf8cc1f2b279494cfdd30a1d62d41f5060 (diff) | |
download | nextcloud-server-946e9ccc0ade60bb9ff34ace9e94e85cce6af96c.tar.gz nextcloud-server-946e9ccc0ade60bb9ff34ace9e94e85cce6af96c.zip |
Merge branch 'master' into fix_for_2377
Diffstat (limited to 'apps/files_encryption/lib')
-rwxr-xr-x | apps/files_encryption/lib/crypt.php | 306 | ||||
-rwxr-xr-x | apps/files_encryption/lib/helper.php | 176 | ||||
-rwxr-xr-x | apps/files_encryption/lib/keymanager.php | 604 | ||||
-rw-r--r-- | apps/files_encryption/lib/proxy.php | 630 | ||||
-rw-r--r-- | apps/files_encryption/lib/session.php | 128 | ||||
-rw-r--r-- | apps/files_encryption/lib/stream.php | 508 | ||||
-rw-r--r-- | apps/files_encryption/lib/util.php | 1502 |
7 files changed, 2676 insertions, 1178 deletions
diff --git a/apps/files_encryption/lib/crypt.php b/apps/files_encryption/lib/crypt.php index 437a18669e5..f5b7a8a0a40 100755 --- a/apps/files_encryption/lib/crypt.php +++ b/apps/files_encryption/lib/crypt.php @@ -25,23 +25,19 @@ namespace OCA\Encryption;
-require_once 'Crypt_Blowfish/Blowfish.php';
-
-// Todo:
-// - Add a setting "Don´t encrypt files larger than xx because of performance"
-// - Don't use a password directly as encryption key. but a key which is
-// stored on the server and encrypted with the user password. -> change pass
-// faster
+//require_once '../3rdparty/Crypt_Blowfish/Blowfish.php';
+require_once realpath( dirname( __FILE__ ) . '/../3rdparty/Crypt_Blowfish/Blowfish.php' );
/**
* Class for common cryptography functionality
*/
-class Crypt {
+class Crypt
+{
/**
* @brief return encryption mode client or server side encryption
- * @param string user name (use system wide setting if name=null)
+ * @param string $user name (use system wide setting if name=null)
* @return string 'client' or 'server'
*/
public static function mode( $user = null ) {
@@ -56,7 +52,7 @@ class Crypt { */
public static function createKeypair() {
- $res = openssl_pkey_new();
+ $res = openssl_pkey_new( array( 'private_key_bits' => 4096 ) );
// Get private key
openssl_pkey_export( $res, $privateKey );
@@ -66,14 +62,14 @@ class Crypt { $publicKey = $publicKey['key'];
- return( array( 'publicKey' => $publicKey, 'privateKey' => $privateKey ) );
+ return ( array( 'publicKey' => $publicKey, 'privateKey' => $privateKey ) );
}
/**
* @brief Add arbitrary padding to encrypted data
* @param string $data data to be padded
- * @return padded data
+ * @return string padded data
* @note In order to end up with data exactly 8192 bytes long we must
* add two letters. It is impossible to achieve exactly 8192 length
* blocks with encryption alone, hence padding is added to achieve the
@@ -90,7 +86,7 @@ class Crypt { /**
* @brief Remove arbitrary padding to encrypted data
* @param string $padded padded data to remove padding from
- * @return unpadded data on success, false on error
+ * @return string unpadded data on success, false on error
*/
public static function removePadding( $padded ) {
@@ -111,10 +107,11 @@ class Crypt { /**
* @brief Check if a file's contents contains an IV and is symmetrically encrypted
- * @return true / false
+ * @param $content
+ * @return boolean
* @note see also OCA\Encryption\Util->isEncryptedPath()
*/
- public static function isCatfile( $content ) {
+ public static function isCatfileContent( $content ) {
if ( !$content ) {
@@ -133,7 +130,7 @@ class Crypt { // Fetch identifier from start of metadata
$identifier = substr( $meta, 0, 6 );
- if ( $identifier == '00iv00') {
+ if ( $identifier == '00iv00' ) {
return true;
@@ -155,7 +152,7 @@ class Crypt { // TODO: Use DI to get \OC\Files\Filesystem out of here
// Fetch all file metadata from DB
- $metadata = \OC\Files\Filesystem::getFileInfo( $path, '' );
+ $metadata = \OC\Files\Filesystem::getFileInfo( $path );
// Return encryption status
return isset( $metadata['encrypted'] ) and ( bool )$metadata['encrypted'];
@@ -164,9 +161,10 @@ class Crypt { /**
* @brief Check if a file is encrypted via legacy system
+ * @param $data
* @param string $relPath The path of the file, relative to user/data;
* e.g. filename or /Docs/filename, NOT admin/files/filename
- * @return true / false
+ * @return boolean
*/
public static function isLegacyEncryptedContent( $data, $relPath ) {
@@ -179,7 +177,7 @@ class Crypt { if (
isset( $metadata['encrypted'] )
and $metadata['encrypted'] === true
- and ! self::isCatfile( $data )
+ and !self::isCatfileContent( $data )
) {
return true;
@@ -194,7 +192,10 @@ class Crypt { /**
* @brief Symmetrically encrypt a string
- * @returns encrypted file
+ * @param $plainContent
+ * @param $iv
+ * @param string $passphrase
+ * @return string encrypted file content
*/
public static function encrypt( $plainContent, $iv, $passphrase = '' ) {
@@ -214,7 +215,11 @@ class Crypt { /**
* @brief Symmetrically decrypt a string
- * @returns decrypted file
+ * @param $encryptedContent
+ * @param $iv
+ * @param $passphrase
+ * @throws \Exception
+ * @return string decrypted file content
*/
public static function decrypt( $encryptedContent, $iv, $passphrase ) {
@@ -222,7 +227,6 @@ class Crypt { return $plainContent;
-
} else {
throw new \Exception( 'Encryption library: Decryption (symmetric) of content failed' );
@@ -237,7 +241,7 @@ class Crypt { * @param string $iv IV to be concatenated
* @returns string concatenated content
*/
- public static function concatIv ( $content, $iv ) {
+ public static function concatIv( $content, $iv ) {
$combined = $content . '00iv00' . $iv;
@@ -250,7 +254,7 @@ class Crypt { * @param string $catFile concatenated data to be split
* @returns array keys: encrypted, iv
*/
- public static function splitIv ( $catFile ) {
+ public static function splitIv( $catFile ) {
// Fetch encryption metadata from end of file
$meta = substr( $catFile, -22 );
@@ -272,8 +276,10 @@ class Crypt { /**
* @brief Symmetrically encrypts a string and returns keyfile content
- * @param $plainContent content to be encrypted in keyfile
- * @returns encrypted content combined with IV
+ * @param string $plainContent content to be encrypted in keyfile
+ * @param string $passphrase
+ * @return bool|string
+ * @return string encrypted content combined with IV
* @note IV need not be specified, as it will be stored in the returned keyfile
* and remain accessible therein.
*/
@@ -309,10 +315,14 @@ class Crypt { /**
* @brief Symmetrically decrypts keyfile content
- * @param string $source
- * @param string $target
- * @param string $key the decryption key
- * @returns decrypted content
+ * @param $keyfileContent
+ * @param string $passphrase
+ * @throws \Exception
+ * @return bool|string
+ * @internal param string $source
+ * @internal param string $target
+ * @internal param string $key the decryption key
+ * @returns string decrypted content
*
* This function decrypts a file
*/
@@ -334,6 +344,8 @@ class Crypt { return $plainContent;
+ } else {
+ return false;
}
}
@@ -350,11 +362,11 @@ class Crypt { $key = self::generateKey();
- if( $encryptedContent = self::symmetricEncryptFileContent( $plainContent, $key ) ) {
+ if ( $encryptedContent = self::symmetricEncryptFileContent( $plainContent, $key ) ) {
return array(
- 'key' => $key
- , 'encrypted' => $encryptedContent
+ 'key' => $key,
+ 'encrypted' => $encryptedContent
);
} else {
@@ -368,22 +380,41 @@ class Crypt { /**
* @brief Create asymmetrically encrypted keyfile content using a generated key
* @param string $plainContent content to be encrypted
- * @returns array keys: key, encrypted
- * @note symmetricDecryptFileContent() can be used to decrypt files created using this method
- *
- * This function decrypts a file
+ * @param array $publicKeys array keys must be the userId of corresponding user
+ * @returns array keys: keys (array, key = userId), data
+ * @note symmetricDecryptFileContent() can decrypt files created using this method
*/
public static function multiKeyEncrypt( $plainContent, array $publicKeys ) {
+ // openssl_seal returns false without errors if $plainContent
+ // is empty, so trigger our own error
+ if ( empty( $plainContent ) ) {
+
+ throw new \Exception( 'Cannot mutliKeyEncrypt empty plain content' );
+
+ }
+
// Set empty vars to be set by openssl by reference
$sealed = '';
- $envKeys = array();
+ $shareKeys = array();
+ $mappedShareKeys = array();
+
+ if ( openssl_seal( $plainContent, $sealed, $shareKeys, $publicKeys ) ) {
+
+ $i = 0;
- if( openssl_seal( $plainContent, $sealed, $envKeys, $publicKeys ) ) {
+ // Ensure each shareKey is labelled with its
+ // corresponding userId
+ foreach ( $publicKeys as $userId => $publicKey ) {
+
+ $mappedShareKeys[$userId] = $shareKeys[$i];
+ $i++;
+
+ }
return array(
- 'keys' => $envKeys
- , 'encrypted' => $sealed
+ 'keys' => $mappedShareKeys,
+ 'data' => $sealed
);
} else {
@@ -396,13 +427,17 @@ class Crypt { /**
* @brief Asymmetrically encrypt a file using multiple public keys
- * @param string $plainContent content to be encrypted
+ * @param $encryptedContent
+ * @param $shareKey
+ * @param $privateKey
+ * @return bool
+ * @internal param string $plainContent content to be encrypted
* @returns string $plainContent decrypted string
* @note symmetricDecryptFileContent() can be used to decrypt files created using this method
*
* This function decrypts a file
*/
- public static function multiKeyDecrypt( $encryptedContent, $envKey, $privateKey ) {
+ public static function multiKeyDecrypt( $encryptedContent, $shareKey, $privateKey ) {
if ( !$encryptedContent ) {
@@ -410,7 +445,7 @@ class Crypt { }
- if ( openssl_open( $encryptedContent, $plainContent, $envKey, $privateKey ) ) {
+ if ( openssl_open( $encryptedContent, $plainContent, $shareKey, $privateKey ) ) {
return $plainContent;
@@ -425,8 +460,8 @@ class Crypt { }
/**
- * @brief Asymmetrically encrypt a string using a public key
- * @returns encrypted file
+ * @brief Asymetrically encrypt a string using a public key
+ * @return string encrypted file
*/
public static function keyEncrypt( $plainContent, $publicKey ) {
@@ -438,110 +473,17 @@ class Crypt { /**
* @brief Asymetrically decrypt a file using a private key
- * @returns decrypted file
+ * @return string decrypted file
*/
public static function keyDecrypt( $encryptedContent, $privatekey ) {
- openssl_private_decrypt( $encryptedContent, $plainContent, $privatekey );
-
- return $plainContent;
-
- }
-
- /**
- * @brief Encrypts content symmetrically and generates keyfile asymmetrically
- * @returns array containing catfile and new keyfile.
- * keys: data, key
- * @note this method is a wrapper for combining other crypt class methods
- */
- public static function keyEncryptKeyfile( $plainContent, $publicKey ) {
-
- // Encrypt plain data, generate keyfile & encrypted file
- $cryptedData = self::symmetricEncryptFileContentKeyfile( $plainContent );
-
- // Encrypt keyfile
- $cryptedKey = self::keyEncrypt( $cryptedData['key'], $publicKey );
-
- return array( 'data' => $cryptedData['encrypted'], 'key' => $cryptedKey );
-
- }
-
- /**
- * @brief Takes catfile, keyfile, and private key, and
- * performs decryption
- * @returns decrypted content
- * @note this method is a wrapper for combining other crypt class methods
- */
- public static function keyDecryptKeyfile( $catfile, $keyfile, $privateKey ) {
-
- // Decrypt the keyfile with the user's private key
- $decryptedKeyfile = self::keyDecrypt( $keyfile, $privateKey );
-
- // Decrypt the catfile symmetrically using the decrypted keyfile
- $decryptedData = self::symmetricDecryptFileContent( $catfile, $decryptedKeyfile );
-
- return $decryptedData;
-
- }
-
- /**
- * @brief Symmetrically encrypt a file by combining encrypted component data blocks
- */
- public static function symmetricBlockEncryptFileContent( $plainContent, $key ) {
-
- $crypted = '';
-
- $remaining = $plainContent;
-
- $testarray = array();
-
- while( strlen( $remaining ) ) {
-
- //echo "\n\n\$block = ".substr( $remaining, 0, 6126 );
-
- // Encrypt a chunk of unencrypted data and add it to the rest
- $block = self::symmetricEncryptFileContent( substr( $remaining, 0, 6126 ), $key );
-
- $padded = self::addPadding( $block );
-
- $crypted .= $block;
-
- $testarray[] = $block;
-
- // Remove the data already encrypted from remaining unencrypted data
- $remaining = substr( $remaining, 6126 );
-
- }
-
- return $crypted;
-
- }
-
-
- /**
- * @brief Symmetrically decrypt a file by combining encrypted component data blocks
- */
- public static function symmetricBlockDecryptFileContent( $crypted, $key ) {
-
- $decrypted = '';
-
- $remaining = $crypted;
-
- $testarray = array();
-
- while( strlen( $remaining ) ) {
-
- $testarray[] = substr( $remaining, 0, 8192 );
-
- // Decrypt a chunk of unencrypted data and add it to the rest
- $decrypted .= self::symmetricDecryptFileContent( $remaining, $key );
-
- // Remove the data already encrypted from remaining unencrypted data
- $remaining = substr( $remaining, 8192 );
+ $result = @openssl_private_decrypt( $encryptedContent, $plainContent, $privatekey );
+ if ( $result ) {
+ return $plainContent;
}
- return $decrypted;
+ return $result;
}
@@ -586,7 +528,7 @@ class Crypt { if ( !$strong ) {
// If OpenSSL indicates randomness is insecure, log error
- throw new \Exception ( 'Encryption library, Insecure symmetric key was generated using openssl_random_pseudo_bytes()' );
+ throw new \Exception( 'Encryption library, Insecure symmetric key was generated using openssl_random_pseudo_bytes()' );
}
@@ -621,6 +563,10 @@ class Crypt { }
+ /**
+ * @param $passphrase
+ * @return mixed
+ */
public static function legacyCreateKey( $passphrase ) {
// Generate a random integer
@@ -635,9 +581,11 @@ class Crypt { /**
* @brief encrypts content using legacy blowfish system
- * @param $content the cleartext message you want to encrypt
- * @param $key the encryption key (optional)
- * @returns encrypted content
+ * @param string $content the cleartext message you want to encrypt
+ * @param string $passphrase
+ * @return
+ * @internal param \OCA\Encryption\the $key encryption key (optional)
+ * @returns string encrypted content
*
* This function encrypts an content
*/
@@ -651,9 +599,11 @@ class Crypt { /**
* @brief decrypts content using legacy blowfish system
- * @param $content the cleartext message you want to decrypt
- * @param $key the encryption key (optional)
- * @returns cleartext content
+ * @param string $content the cleartext message you want to decrypt
+ * @param string $passphrase
+ * @return string
+ * @internal param \OCA\Encryption\the $key encryption key (optional)
+ * @return string cleartext content
*
* This function decrypts an content
*/
@@ -663,33 +613,49 @@ class Crypt { $decrypted = $bf->decrypt( $content );
- $trimmed = rtrim( $decrypted, "\0" );
-
- return $trimmed;
+ return rtrim( $decrypted, "\0" );;
}
- public static function legacyKeyRecryptKeyfile( $legacyEncryptedContent, $legacyPassphrase, $publicKey, $newPassphrase ) {
-
- $decrypted = self::legacyDecrypt( $legacyEncryptedContent, $legacyPassphrase );
-
- $recrypted = self::keyEncryptKeyfile( $decrypted, $publicKey );
-
- return $recrypted;
-
+ /**
+ * @param $data
+ * @param string $key
+ * @param int $maxLength
+ * @return string
+ */
+ private static function legacyBlockDecrypt( $data, $key = '', $maxLength = 0 ) {
+ $result = '';
+ while ( strlen( $data ) ) {
+ $result .= self::legacyDecrypt( substr( $data, 0, 8192 ), $key );
+ $data = substr( $data, 8192 );
+ }
+ if ( $maxLength > 0 ) {
+ return substr( $result, 0, $maxLength );
+ } else {
+ return rtrim( $result, "\0" );
+ }
}
/**
- * @brief Re-encryptes a legacy blowfish encrypted file using AES with integrated IV
- * @param $legacyContent the legacy encrypted content to re-encrypt
- * @returns cleartext content
- *
- * This function decrypts an content
+ * @param $legacyEncryptedContent
+ * @param $legacyPassphrase
+ * @param $publicKeys
+ * @param $newPassphrase
+ * @param $path
+ * @return array
*/
- public static function legacyRecrypt( $legacyContent, $legacyPassphrase, $newPassphrase ) {
+ public static function legacyKeyRecryptKeyfile( $legacyEncryptedContent, $legacyPassphrase, $publicKeys, $newPassphrase, $path ) {
+
+ $decrypted = self::legacyBlockDecrypt( $legacyEncryptedContent, $legacyPassphrase );
+
+ // Encrypt plain data, generate keyfile & encrypted file
+ $cryptedData = self::symmetricEncryptFileContentKeyfile( $decrypted );
+
+ // Encrypt plain keyfile to multiple sharefiles
+ $multiEncrypted = Crypt::multiKeyEncrypt( $cryptedData['key'], $publicKeys );
- // TODO: write me
+ return array( 'data' => $cryptedData['encrypted'], 'filekey' => $multiEncrypted['data'], 'sharekeys' => $multiEncrypted['keys'] );
}
-} +}
\ No newline at end of file diff --git a/apps/files_encryption/lib/helper.php b/apps/files_encryption/lib/helper.php new file mode 100755 index 00000000000..7a2d19eed57 --- /dev/null +++ b/apps/files_encryption/lib/helper.php @@ -0,0 +1,176 @@ +<?php + +/** + * ownCloud + * + * @author Florin Peter + * @copyright 2013 Florin Peter <owncloud@florin-peter.de> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Encryption; + + /** + * @brief Class to manage registration of hooks an various helper methods + */ +/** + * Class Helper + * @package OCA\Encryption + */ +class Helper +{ + + /** + * @brief register share related hooks + * + */ + public static function registerShareHooks() { + + \OCP\Util::connectHook( 'OCP\Share', 'pre_shared', 'OCA\Encryption\Hooks', 'preShared' ); + \OCP\Util::connectHook( 'OCP\Share', 'post_shared', 'OCA\Encryption\Hooks', 'postShared' ); + \OCP\Util::connectHook( 'OCP\Share', 'post_unshare', 'OCA\Encryption\Hooks', 'postUnshare' ); + } + + /** + * @brief register user related hooks + * + */ + public static function registerUserHooks() { + + \OCP\Util::connectHook( 'OC_User', 'post_login', 'OCA\Encryption\Hooks', 'login' ); + \OCP\Util::connectHook( 'OC_User', 'post_setPassword', 'OCA\Encryption\Hooks', 'setPassphrase' ); + \OCP\Util::connectHook( 'OC_User', 'post_createUser', 'OCA\Encryption\Hooks', 'postCreateUser' ); + \OCP\Util::connectHook( 'OC_User', 'post_deleteUser', 'OCA\Encryption\Hooks', 'postDeleteUser' ); + } + + /** + * @brief register filesystem related hooks + * + */ + public static function registerFilesystemHooks() { + + \OCP\Util::connectHook( 'OC_Filesystem', 'post_rename', 'OCA\Encryption\Hooks', 'postRename' ); + } + + /** + * @brief setup user for files_encryption + * + * @param Util $util + * @param string $password + * @return bool + */ + public static function setupUser( $util, $password ) { + // Check files_encryption infrastructure is ready for action + if ( !$util->ready() ) { + + \OC_Log::write( 'Encryption library', 'User account "' . $util->getUserId() . '" is not ready for encryption; configuration started', \OC_Log::DEBUG ); + + if ( !$util->setupServerSide( $password ) ) { + return false; + } + } + + return true; + } + + /** + * @brief enable recovery + * + * @param $recoveryKeyId + * @param $recoveryPassword + * @internal param \OCA\Encryption\Util $util + * @internal param string $password + * @return bool + */ + public static function adminEnableRecovery( $recoveryKeyId, $recoveryPassword ) { + $view = new \OC\Files\View( '/' ); + + if ( $recoveryKeyId === null ) { + $recoveryKeyId = 'recovery_' . substr( md5( time() ), 0, 8 ); + \OC_Appconfig::setValue( 'files_encryption', 'recoveryKeyId', $recoveryKeyId ); + } + + if ( !$view->is_dir( '/owncloud_private_key' ) ) { + $view->mkdir( '/owncloud_private_key' ); + } + + if ( + ( !$view->file_exists( "/public-keys/" . $recoveryKeyId . ".public.key" ) + || !$view->file_exists( "/owncloud_private_key/" . $recoveryKeyId . ".private.key" ) ) + ) { + + $keypair = \OCA\Encryption\Crypt::createKeypair(); + + \OC_FileProxy::$enabled = false; + + // Save public key + + if ( !$view->is_dir( '/public-keys' ) ) { + $view->mkdir( '/public-keys' ); + } + + $view->file_put_contents( '/public-keys/' . $recoveryKeyId . '.public.key', $keypair['publicKey'] ); + + // Encrypt private key empthy passphrase + $encryptedPrivateKey = \OCA\Encryption\Crypt::symmetricEncryptFileContent( $keypair['privateKey'], $recoveryPassword ); + + // Save private key + $view->file_put_contents( '/owncloud_private_key/' . $recoveryKeyId . '.private.key', $encryptedPrivateKey ); + + // create control file which let us check later on if the entered password was correct. + $encryptedControlData = \OCA\Encryption\Crypt::keyEncrypt( "ownCloud", $keypair['publicKey'] ); + if ( !$view->is_dir( '/control-file' ) ) { + $view->mkdir( '/control-file' ); + } + $view->file_put_contents( '/control-file/controlfile.enc', $encryptedControlData ); + + \OC_FileProxy::$enabled = true; + + // Set recoveryAdmin as enabled + \OC_Appconfig::setValue( 'files_encryption', 'recoveryAdminEnabled', 1 ); + + $return = true; + + } else { // get recovery key and check the password + $util = new \OCA\Encryption\Util( new \OC_FilesystemView( '/' ), \OCP\User::getUser() ); + $return = $util->checkRecoveryPassword( $recoveryPassword ); + if ( $return ) { + \OC_Appconfig::setValue( 'files_encryption', 'recoveryAdminEnabled', 1 ); + } + } + + return $return; + } + + + /** + * @brief disable recovery + * + * @param $recoveryPassword + * @return bool + */ + public static function adminDisableRecovery( $recoveryPassword ) { + $util = new Util( new \OC_FilesystemView( '/' ), \OCP\User::getUser() ); + $return = $util->checkRecoveryPassword( $recoveryPassword ); + + if ( $return ) { + // Set recoveryAdmin as disabled + \OC_Appconfig::setValue( 'files_encryption', 'recoveryAdminEnabled', 0 ); + } + + return $return; + } +}
\ No newline at end of file diff --git a/apps/files_encryption/lib/keymanager.php b/apps/files_encryption/lib/keymanager.php index 95587797154..aaa2e4ba1b5 100755 --- a/apps/files_encryption/lib/keymanager.php +++ b/apps/files_encryption/lib/keymanager.php @@ -27,20 +27,28 @@ namespace OCA\Encryption; * @brief Class to manage storage and retrieval of encryption keys * @note Where a method requires a view object, it's root must be '/' */ -class Keymanager { - +class Keymanager +{ + /** * @brief retrieve the ENCRYPTED private key from a user - * - * @return string private key or false + * + * @param \OC_FilesystemView $view + * @param string $user + * @return string private key or false (hopefully) * @note the key returned by this method must be decrypted before use */ public static function getPrivateKey( \OC_FilesystemView $view, $user ) { - - $path = '/' . $user . '/' . 'files_encryption' . '/' . $user.'.private.key'; - + + $path = '/' . $user . '/' . 'files_encryption' . '/' . $user . '.private.key'; + + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + $key = $view->file_get_contents( $path ); - + + \OC_FileProxy::$enabled = $proxyStatus; + return $key; } @@ -51,101 +59,150 @@ class Keymanager { * @return string public key or false */ public static function getPublicKey( \OC_FilesystemView $view, $userId ) { - - return $view->file_get_contents( '/public-keys/' . '/' . $userId . '.public.key' ); - + + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + $result = $view->file_get_contents( '/public-keys/' . $userId . '.public.key' ); + + \OC_FileProxy::$enabled = $proxyStatus; + + return $result; + } - + /** - * @brief retrieve both keys from a user (private and public) + * @brief Retrieve a user's public and private key * @param \OC_FilesystemView $view * @param $userId * @return array keys: privateKey, publicKey */ public static function getUserKeys( \OC_FilesystemView $view, $userId ) { - + return array( 'publicKey' => self::getPublicKey( $view, $userId ) - , 'privateKey' => self::getPrivateKey( $view, $userId ) + , 'privateKey' => self::getPrivateKey( $view, $userId ) ); - + } - + /** - * @brief Retrieve public keys of all users with access to a file - * @param string $path Path to file - * @return array of public keys for the given file - * @note Checks that the sharing app is enabled should be performed - * by client code, that isn't checked here + * @brief Retrieve public keys for given users + * @param \OC_FilesystemView $view + * @param array $userIds + * @return array of public keys for the specified users */ - public static function getPublicKeys( \OC_FilesystemView $view, $userId, $filePath ) { - - $path = ltrim( $path, '/' ); - - $filepath = '/' . $userId . '/files/' . $filePath; - - // Check if sharing is enabled - if ( OC_App::isEnabled( 'files_sharing' ) ) { - - - - } else { - - // check if it is a file owned by the user and not shared at all - $userview = new \OC_FilesystemView( '/'.$userId.'/files/' ); - - if ( $userview->file_exists( $path ) ) { - - $users[] = $userId; - - } - - } - - $view = new \OC_FilesystemView( '/public-keys/' ); - - $keylist = array(); - - $count = 0; - - foreach ( $users as $user ) { - - $keylist['key'.++$count] = $view->file_get_contents( $user.'.public.key' ); - + public static function getPublicKeys( \OC_FilesystemView $view, array $userIds ) { + + $keys = array(); + + foreach ( $userIds as $userId ) { + + $keys[$userId] = self::getPublicKey( $view, $userId ); + } - - return $keylist; - + + return $keys; + } - + /** * @brief store file encryption key * + * @param \OC_FilesystemView $view * @param string $path relative path of the file, including filename - * @param string $key + * @param $userId + * @param $catfile + * @internal param string $key * @return bool true/false - * @note The keyfile is not encrypted here. Client code must + * @note The keyfile is not encrypted here. Client code must * asymmetrically encrypt the keyfile before passing it to this method */ public static function setFileKey( \OC_FilesystemView $view, $path, $userId, $catfile ) { - - $basePath = '/' . $userId . '/files_encryption/keyfiles'; - - $targetPath = self::keySetPreparation( $view, $path, $basePath, $userId ); - - if ( $view->is_dir( $basePath . '/' . $targetPath ) ) { - - - + + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + //here we need the currently logged in user, while userId can be a different user + $util = new Util( $view, \OCP\User::getUser() ); + list( $owner, $filename ) = $util->getUidAndFilename( $path ); + + $basePath = '/' . $owner . '/files_encryption/keyfiles'; + + $targetPath = self::keySetPreparation( $view, $filename, $basePath, $owner ); + + if ( !$view->is_dir( $basePath . '/' . $targetPath ) ) { + + // create all parent folders + $info = pathinfo( $basePath . '/' . $targetPath ); + $keyfileFolderName = $view->getLocalFolder( $info['dirname'] ); + + if ( !file_exists( $keyfileFolderName ) ) { + + mkdir( $keyfileFolderName, 0750, true ); + + } + } + + // try reusing key file if part file + if ( self::isPartialFilePath( $targetPath ) ) { + + $result = $view->file_put_contents( $basePath . '/' . self::fixPartialFilePath( $targetPath ) . '.key', $catfile ); + } else { - // Save the keyfile in parallel directory - return $view->file_put_contents( $basePath . '/' . $targetPath . '.key', $catfile ); - + $result = $view->file_put_contents( $basePath . '/' . $targetPath . '.key', $catfile ); + } - + + \OC_FileProxy::$enabled = $proxyStatus; + + return $result; + } - + + /** + * @brief Remove .path extension from a file path + * @param string $path Path that may identify a .part file + * @return string File path without .part extension + * @note this is needed for reusing keys + */ + public static function fixPartialFilePath( $path ) { + + if ( preg_match( '/\.part$/', $path ) ) { + + $newLength = strlen( $path ) - 5; + $fPath = substr( $path, 0, $newLength ); + + return $fPath; + + } else { + + return $path; + + } + + } + + /** + * @brief Check if a path is a .part file + * @param string $path Path that may identify a .part file + * @return bool + */ + public static function isPartialFilePath( $path ) { + + if ( preg_match( '/\.part$/', $path ) ) { + + return true; + + } else { + + return false; + + } + + } + /** * @brief retrieve keyfile for an encrypted file * @param \OC_FilesystemView $view @@ -157,27 +214,50 @@ class Keymanager { * of the keyfile must be performed by client code */ public static function getFileKey( \OC_FilesystemView $view, $userId, $filePath ) { - - $filePath_f = ltrim( $filePath, '/' ); - - $catfilePath = '/' . $userId . '/files_encryption/keyfiles/' . $filePath_f . '.key'; - - if ( $view->file_exists( $catfilePath ) ) { - - return $view->file_get_contents( $catfilePath ); - + + // try reusing key file if part file + if ( self::isPartialFilePath( $filePath ) ) { + + $result = self::getFileKey( $view, $userId, self::fixPartialFilePath( $filePath ) ); + + if ( $result ) { + + return $result; + + } + + } + + $util = new Util( $view, \OCP\User::getUser() ); + + list( $owner, $filename ) = $util->getUidAndFilename( $filePath ); + $filePath_f = ltrim( $filename, '/' ); + + $keyfilePath = '/' . $owner . '/files_encryption/keyfiles/' . $filePath_f . '.key'; + + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + if ( $view->file_exists( $keyfilePath ) ) { + + $result = $view->file_get_contents( $keyfilePath ); + } else { - - return false; - + + $result = false; + } - + + \OC_FileProxy::$enabled = $proxyStatus; + + return $result; + } - + /** * @brief Delete a keyfile * - * @param OC_FilesystemView $view + * @param \OC_FilesystemView $view * @param string $userId username * @param string $path path of the file the key belongs to * @return bool Outcome of unlink operation @@ -185,139 +265,299 @@ class Keymanager { * /data/admin/files/mydoc.txt */ public static function deleteFileKey( \OC_FilesystemView $view, $userId, $path ) { - + $trimmed = ltrim( $path, '/' ); - $keyPath = '/' . $userId . '/files_encryption/keyfiles/' . $trimmed . '.key'; - - // Unlink doesn't tell us if file was deleted (not found returns - // true), so we perform our own test - if ( $view->file_exists( $keyPath ) ) { - - return $view->unlink( $keyPath ); - - } else { - + $keyPath = '/' . $userId . '/files_encryption/keyfiles/' . $trimmed; + + $result = false; + + if ( $view->is_dir( $keyPath ) ) { + + $result = $view->unlink( $keyPath ); + + } else if ( $view->file_exists( $keyPath . '.key' ) ) { + + $result = $view->unlink( $keyPath . '.key' ); + + } + + if ( !$result ) { + \OC_Log::write( 'Encryption library', 'Could not delete keyfile; does not exist: "' . $keyPath, \OC_Log::ERROR ); - - return false; - + } - + + return $result; + } - + /** * @brief store private key from the user - * @param string key + * @param string $key * @return bool * @note Encryption of the private key must be performed by client code * as no encryption takes place here */ public static function setPrivateKey( $key ) { - + $user = \OCP\User::getUser(); - + $view = new \OC_FilesystemView( '/' . $user . '/files_encryption' ); - + + $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - + if ( !$view->file_exists( '' ) ) $view->mkdir( '' ); - - return $view->file_put_contents( $user . '.private.key', $key ); + + $result = $view->file_put_contents( $user . '.private.key', $key ); + + \OC_FileProxy::$enabled = $proxyStatus; + + return $result; } - + /** - * @brief store private keys from the user + * @brief store share key * - * @param string privatekey - * @param string publickey + * @param \OC_FilesystemView $view + * @param string $path relative path of the file, including filename + * @param $userId + * @param $shareKey + * @internal param string $key + * @internal param string $dbClassName * @return bool true/false + * @note The keyfile is not encrypted here. Client code must + * asymmetrically encrypt the keyfile before passing it to this method */ - public static function setUserKeys($privatekey, $publickey) { - - return ( self::setPrivateKey( $privatekey ) && self::setPublicKey( $publickey ) ); - + public static function setShareKey( \OC_FilesystemView $view, $path, $userId, $shareKey ) { + + // Here we need the currently logged in user, while userId can be a different user + $util = new Util( $view, \OCP\User::getUser() ); + + list( $owner, $filename ) = $util->getUidAndFilename( $path ); + + $basePath = '/' . $owner . '/files_encryption/share-keys'; + + $shareKeyPath = self::keySetPreparation( $view, $filename, $basePath, $owner ); + + // try reusing key file if part file + if ( self::isPartialFilePath( $shareKeyPath ) ) { + + $writePath = $basePath . '/' . self::fixPartialFilePath( $shareKeyPath ) . '.' . $userId . '.shareKey'; + + } else { + + $writePath = $basePath . '/' . $shareKeyPath . '.' . $userId . '.shareKey'; + + } + + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + $result = $view->file_put_contents( $writePath, $shareKey ); + + \OC_FileProxy::$enabled = $proxyStatus; + + if ( + is_int( $result ) + && $result > 0 + ) { + + return true; + + } else { + + return false; + + } + } - + /** - * @brief store public key of the user - * - * @param string key - * @return bool true/false + * @brief store multiple share keys for a single file + * @param \OC_FilesystemView $view + * @param $path + * @param array $shareKeys + * @return bool + */ + public static function setShareKeys( \OC_FilesystemView $view, $path, array $shareKeys ) { + + // $shareKeys must be an array with the following format: + // [userId] => [encrypted key] + + $result = true; + + foreach ( $shareKeys as $userId => $shareKey ) { + + if ( !self::setShareKey( $view, $path, $userId, $shareKey ) ) { + + // If any of the keys are not set, flag false + $result = false; + + } + + } + + // Returns false if any of the keys weren't set + return $result; + + } + + /** + * @brief retrieve shareKey for an encrypted file + * @param \OC_FilesystemView $view + * @param string $userId + * @param string $filePath + * @internal param \OCA\Encryption\file $string name + * @return string file key or false + * @note The sharekey returned is encrypted. Decryption + * of the keyfile must be performed by client code */ - public static function setPublicKey( $key ) { - - $view = new \OC_FilesystemView( '/public-keys' ); - + public static function getShareKey( \OC_FilesystemView $view, $userId, $filePath ) { + + // try reusing key file if part file + if ( self::isPartialFilePath( $filePath ) ) { + + $result = self::getShareKey( $view, $userId, self::fixPartialFilePath( $filePath ) ); + + if ( $result ) { + + return $result; + + } + + } + + $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - - if ( !$view->file_exists( '' ) ) - $view->mkdir( '' ); - - return $view->file_put_contents( \OCP\User::getUser() . '.public.key', $key ); - + //here we need the currently logged in user, while userId can be a different user + $util = new Util( $view, \OCP\User::getUser() ); + + list( $owner, $filename ) = $util->getUidAndFilename( $filePath ); + $shareKeyPath = \OC\Files\Filesystem::normalizePath( '/' . $owner . '/files_encryption/share-keys/' . $filename . '.' . $userId . '.shareKey' ); + + if ( $view->file_exists( $shareKeyPath ) ) { + + $result = $view->file_get_contents( $shareKeyPath ); + + } else { + + $result = false; + + } + + \OC_FileProxy::$enabled = $proxyStatus; + + return $result; + } - + /** - * @brief store file encryption key + * @brief delete all share keys of a given file + * @param \OC_FilesystemView $view + * @param string $userId owner of the file + * @param string $filePath path to the file, relative to the owners file dir + */ + public static function delAllShareKeys( \OC_FilesystemView $view, $userId, $filePath ) { + + if ( $view->is_dir( $userId . '/files/' . $filePath ) ) { + $view->unlink( $userId . '/files_encryption/share-keys/' . $filePath ); + } else { + $localKeyPath = $view->getLocalFile( $userId . '/files_encryption/share-keys/' . $filePath ); + $matches = glob( preg_quote( $localKeyPath ) . '*.shareKey' ); + foreach ( $matches as $ma ) { + $result = unlink( $ma ); + if ( !$result ) { + \OC_Log::write( 'Encryption library', 'Keyfile or shareKey could not be deleted for file "' . $filePath . '"', \OC_Log::ERROR ); + } + } + } + } + + /** + * @brief Delete a single user's shareKey for a single file + */ + public static function delShareKey( \OC_FilesystemView $view, $userIds, $filePath ) { + + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + //here we need the currently logged in user, while userId can be a different user + $util = new Util( $view, \OCP\User::getUser() ); + + list( $owner, $filename ) = $util->getUidAndFilename( $filePath ); + + $shareKeyPath = \OC\Files\Filesystem::normalizePath( '/' . $owner . '/files_encryption/share-keys/' . $filename ); + + if ( $view->is_dir( $shareKeyPath ) ) { + + $localPath = \OC\Files\Filesystem::normalizePath( $view->getLocalFolder( $shareKeyPath ) ); + self::recursiveDelShareKeys( $localPath, $userIds ); + + } else { + + foreach ( $userIds as $userId ) { + + if ( !$view->unlink( $shareKeyPath . '.' . $userId . '.shareKey' ) ) { + \OC_Log::write( 'Encryption library', 'Could not delete shareKey; does not exist: "' . $shareKeyPath . '.' . $userId . '.shareKey"', \OC_Log::ERROR ); + } + + } + } + + \OC_FileProxy::$enabled = $proxyStatus; + } + + /** + * @brief recursively delete share keys from given users * - * @param string $path relative path of the file, including filename - * @param string $key - * @param null $view - * @param string $dbClassName - * @return bool true/false - * @note The keyfile is not encrypted here. Client code must - * asymmetrically encrypt the keyfile before passing it to this method + * @param string $dir directory + * @param array $userIds user ids for which the share keys should be deleted */ - public static function setShareKey( \OC_FilesystemView $view, $path, $userId, $shareKey ) { - - $basePath = '/' . $userId . '/files_encryption/share-keys'; - - $shareKeyPath = self::keySetPreparation( $view, $path, $basePath, $userId ); - - return $view->file_put_contents( $basePath . '/' . $shareKeyPath . '.shareKey', $shareKey ); - + private static function recursiveDelShareKeys( $dir, $userIds ) { + foreach ( $userIds as $userId ) { + $matches = glob( preg_quote( $dir ) . '/*' . preg_quote( '.' . $userId . '.shareKey' ) ); + } + /** @var $matches array */ + foreach ( $matches as $ma ) { + if ( !unlink( $ma ) ) { + \OC_Log::write( 'Encryption library', 'Could not delete shareKey; does not exist: "' . $ma . '"', \OC_Log::ERROR ); + } + } + $subdirs = $directories = glob( preg_quote( $dir ) . '/*', GLOB_ONLYDIR ); + foreach ( $subdirs as $subdir ) { + self::recursiveDelShareKeys( $subdir, $userIds ); + } } - + /** * @brief Make preparations to vars and filesystem for saving a keyfile */ public static function keySetPreparation( \OC_FilesystemView $view, $path, $basePath, $userId ) { - + $targetPath = ltrim( $path, '/' ); - + $path_parts = pathinfo( $targetPath ); - + // If the file resides within a subdirectory, create it - if ( - isset( $path_parts['dirname'] ) - && ! $view->file_exists( $basePath . '/' . $path_parts['dirname'] ) + if ( + isset( $path_parts['dirname'] ) + && !$view->file_exists( $basePath . '/' . $path_parts['dirname'] ) ) { - - $view->mkdir( $basePath . '/' . $path_parts['dirname'] ); - + $sub_dirs = explode( DIRECTORY_SEPARATOR, $basePath . '/' . $path_parts['dirname'] ); + $dir = ''; + foreach ( $sub_dirs as $sub_dir ) { + $dir .= '/' . $sub_dir; + if ( !$view->is_dir( $dir ) ) { + $view->mkdir( $dir ); + } + } } - + return $targetPath; - - } - /** - * @brief Fetch the legacy encryption key from user files - * @param string $login used to locate the legacy key - * @param string $passphrase used to decrypt the legacy key - * @return true / false - * - * if the key is left out, the default handler will be used - */ - public function getLegacyKey() { - - $user = \OCP\User::getUser(); - $view = new \OC_FilesystemView( '/' . $user ); - return $view->file_get_contents( 'encryption.key' ); - } - }
\ No newline at end of file diff --git a/apps/files_encryption/lib/proxy.php b/apps/files_encryption/lib/proxy.php index 55cddf2bec8..eaaeae9b619 100644 --- a/apps/files_encryption/lib/proxy.php +++ b/apps/files_encryption/lib/proxy.php @@ -1,41 +1,46 @@ <?php /** -* ownCloud -* -* @author Sam Tuke, Robin Appelman -* @copyright 2012 Sam Tuke samtuke@owncloud.com, Robin Appelman -* icewind1991@gmail.com -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE -* License as published by the Free Software Foundation; either -* version 3 of the License, or any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU AFFERO GENERAL PUBLIC LICENSE for more details. -* -* You should have received a copy of the GNU Affero General Public -* License along with this library. If not, see <http://www.gnu.org/licenses/>. -* -*/ + * ownCloud + * + * @author Sam Tuke, Robin Appelman + * @copyright 2012 Sam Tuke samtuke@owncloud.com, Robin Appelman + * icewind1991@gmail.com + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + */ /** -* @brief Encryption proxy which handles filesystem operations before and after -* execution and encrypts, and handles keyfiles accordingly. Used for -* webui. -*/ + * @brief Encryption proxy which handles filesystem operations before and after + * execution and encrypts, and handles keyfiles accordingly. Used for + * webui. + */ namespace OCA\Encryption; -class Proxy extends \OC_FileProxy { +/** + * Class Proxy + * @package OCA\Encryption + */ +class Proxy extends \OC_FileProxy +{ private static $blackList = null; //mimetypes blacklisted from encryption - + private static $enableEncryption = null; - + /** * Check if a file requires encryption * @param string $path @@ -44,346 +49,417 @@ class Proxy extends \OC_FileProxy { * Tests if server side encryption is enabled, and file is allowed by blacklists */ private static function shouldEncrypt( $path ) { - + if ( is_null( self::$enableEncryption ) ) { - - if ( - \OCP\Config::getAppValue( 'files_encryption', 'enable_encryption', 'true' ) == 'true' - && Crypt::mode() == 'server' + + if ( + \OCP\Config::getAppValue( 'files_encryption', 'enable_encryption', 'true' ) == 'true' + && Crypt::mode() == 'server' ) { - + self::$enableEncryption = true; - + } else { - + self::$enableEncryption = false; - + } - + } - + if ( !self::$enableEncryption ) { - + return false; - + } - - if ( is_null(self::$blackList ) ) { - - self::$blackList = explode(',', \OCP\Config::getAppValue( 'files_encryption', 'type_blacklist', 'jpg,png,jpeg,avi,mpg,mpeg,mkv,mp3,oga,ogv,ogg' ) ); - + + if ( is_null( self::$blackList ) ) { + + self::$blackList = explode( ',', \OCP\Config::getAppValue( 'files_encryption', 'type_blacklist', '' ) ); + } - - if ( Crypt::isCatfile( $path ) ) { - + + if ( Crypt::isCatfileContent( $path ) ) { + return true; - + } - - $extension = substr( $path, strrpos( $path, '.' ) +1 ); - + + $extension = substr( $path, strrpos( $path, '.' ) + 1 ); + if ( array_search( $extension, self::$blackList ) === false ) { - + return true; - + } - + return false; } - + + /** + * @param $path + * @param $data + * @return bool + */ public function preFile_put_contents( $path, &$data ) { - + if ( self::shouldEncrypt( $path ) ) { - - if ( !is_resource( $data ) ) { //stream put contents should have been converted to fopen - + + // Stream put contents should have been converted to fopen + if ( !is_resource( $data ) ) { + $userId = \OCP\USER::getUser(); - - $rootView = new \OC_FilesystemView( '/' ); - + $view = new \OC_FilesystemView( '/' ); + $util = new Util( $view, $userId ); + $session = new Session( $view ); + $privateKey = $session->getPrivateKey(); + $filePath = $util->stripUserFilesPath( $path ); // Set the filesize for userland, before encrypting $size = strlen( $data ); - + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - - // TODO: Check if file is shared, if so, use multiKeyEncrypt - - // Encrypt plain data and fetch key - $encrypted = Crypt::keyEncryptKeyfile( $data, Keymanager::getPublicKey( $rootView, $userId ) ); - - // Replace plain content with encrypted content by reference - $data = $encrypted['data']; - - $filePath = explode( '/', $path ); - - $filePath = array_slice( $filePath, 3 ); - - $filePath = '/' . implode( '/', $filePath ); - - // TODO: make keyfile dir dynamic from app config - - $view = new \OC_FilesystemView( '/' ); - + + // Check if there is an existing key we can reuse + if ( $encKeyfile = Keymanager::getFileKey( $view, $userId, $filePath ) ) { + + // Fetch shareKey + $shareKey = Keymanager::getShareKey( $view, $userId, $filePath ); + + // Decrypt the keyfile + $plainKey = Crypt::multiKeyDecrypt( $encKeyfile, $shareKey, $privateKey ); + + } else { + + // Make a new key + $plainKey = Crypt::generateKey(); + + } + + // Encrypt data + $encData = Crypt::symmetricEncryptFileContent( $data, $plainKey ); + + $sharingEnabled = \OCP\Share::isEnabled(); + + // if file exists try to get sharing users + if ( $view->file_exists( $path ) ) { + $uniqueUserIds = $util->getSharingUsersArray( $sharingEnabled, $filePath, $userId ); + } else { + $uniqueUserIds[] = $userId; + } + + // Fetch public keys for all users who will share the file + $publicKeys = Keymanager::getPublicKeys( $view, $uniqueUserIds ); + + // Encrypt plain keyfile to multiple sharefiles + $multiEncrypted = Crypt::multiKeyEncrypt( $plainKey, $publicKeys ); + + // Save sharekeys to user folders + Keymanager::setShareKeys( $view, $filePath, $multiEncrypted['keys'] ); + + // Set encrypted keyfile as common varname + $encKey = $multiEncrypted['data']; + // Save keyfile for newly encrypted file in parallel directory tree - Keymanager::setFileKey( $view, $filePath, $userId, $encrypted['key'] ); - + Keymanager::setFileKey( $view, $filePath, $userId, $encKey ); + + // Replace plain content with encrypted content by reference + $data = $encData; + // Update the file cache with file info - \OC\Files\Filesystem::putFileInfo( $path, array( 'encrypted'=>true, 'size' => $size ), '' ); - + \OC\Files\Filesystem::putFileInfo( $filePath, array( 'encrypted' => true, 'size' => strlen( $data ), 'unencrypted_size' => $size ), '' ); + // Re-enable proxy - our work is done - \OC_FileProxy::$enabled = true; - + \OC_FileProxy::$enabled = $proxyStatus; + } } - + + return true; + } - + /** * @param string $path Path of file from which has been read * @param string $data Data that has been read from file */ public function postFile_get_contents( $path, $data ) { - - // TODO: Use dependency injection to add required args for view and user etc. to this method + + $userId = \OCP\USER::getUser(); + $view = new \OC_FilesystemView( '/' ); + $util = new Util( $view, $userId ); + + $relPath = $util->stripUserFilesPath( $path ); // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - + + // init session + $session = new Session( $view ); + // If data is a catfile - if ( - Crypt::mode() == 'server' - && Crypt::isCatfile( $data ) + if ( + Crypt::mode() == 'server' + && Crypt::isCatfileContent( $data ) ) { - - $split = explode( '/', $path ); - - $filePath = array_slice( $split, 3 ); - - $filePath = '/' . implode( '/', $filePath ); - - //$cached = \OC\Files\Filesystem::getFileInfo( $path, '' ); - - $view = new \OC_FilesystemView( '' ); - - $userId = \OCP\USER::getUser(); - - // TODO: Check if file is shared, if so, use multiKeyDecrypt - - $encryptedKeyfile = Keymanager::getFileKey( $view, $userId, $filePath ); - - $session = new Session(); - - $decrypted = Crypt::keyDecryptKeyfile( $data, $encryptedKeyfile, $session->getPrivateKey( $split[1] ) ); - + + $privateKey = $session->getPrivateKey( $userId ); + + // Get the encrypted keyfile + $encKeyfile = Keymanager::getFileKey( $view, $userId, $relPath ); + + // Attempt to fetch the user's shareKey + $shareKey = Keymanager::getShareKey( $view, $userId, $relPath ); + + // Decrypt keyfile with shareKey + $plainKeyfile = Crypt::multiKeyDecrypt( $encKeyfile, $shareKey, $privateKey ); + + $plainData = Crypt::symmetricDecryptFileContent( $data, $plainKeyfile ); + } elseif ( - Crypt::mode() == 'server' - && isset( $_SESSION['legacyenckey'] ) - && Crypt::isEncryptedMeta( $path ) + Crypt::mode() == 'server' + && isset( $_SESSION['legacyenckey'] ) + && Crypt::isEncryptedMeta( $path ) ) { - - $decrypted = Crypt::legacyDecrypt( $data, $_SESSION['legacyenckey'] ); - + $plainData = Crypt::legacyDecrypt( $data, $session->getLegacyKey() ); } - - \OC_FileProxy::$enabled = true; - - if ( ! isset( $decrypted ) ) { - - $decrypted = $data; - + + \OC_FileProxy::$enabled = $proxyStatus; + + if ( !isset( $plainData ) ) { + + $plainData = $data; + } - - return $decrypted; - + + return $plainData; + } - + /** * @brief When a file is deleted, remove its keyfile also */ public function preUnlink( $path ) { - + + // let the trashbin handle this + if ( \OCP\App::isEnabled( 'files_trashbin' ) ) { + return true; + } + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - + $view = new \OC_FilesystemView( '/' ); - + $userId = \OCP\USER::getUser(); - + + $util = new Util( $view, $userId ); + // Format path to be relative to user files dir - $trimmed = ltrim( $path, '/' ); - $split = explode( '/', $trimmed ); - $sliced = array_slice( $split, 2 ); - $relPath = implode( '/', $sliced ); - - if ( $view->is_dir( $path ) ) { - - // Dirs must be handled separately as deleteFileKey - // doesn't handle them - $view->unlink( $userId . '/' . 'files_encryption' . '/' . 'keyfiles' . '/'. $relPath ); - - } else { - - // Delete keyfile so it isn't orphaned - $result = Keymanager::deleteFileKey( $view, $userId, $relPath ); - - \OC_FileProxy::$enabled = true; - - return $result; - + $relPath = $util->stripUserFilesPath( $path ); + + list( $owner, $ownerPath ) = $util->getUidAndFilename( $relPath ); + + // Delete keyfile & shareKey so it isn't orphaned + if ( !Keymanager::deleteFileKey( $view, $owner, $ownerPath ) ) { + \OC_Log::write( 'Encryption library', 'Keyfile or shareKey could not be deleted for file "' . $ownerPath . '"', \OC_Log::ERROR ); } - + + Keymanager::delAllShareKeys( $view, $owner, $ownerPath ); + + \OC_FileProxy::$enabled = $proxyStatus; + + // If we don't return true then file delete will fail; better + // to leave orphaned keyfiles than to disallow file deletion + return true; + } /** - * @brief When a file is renamed, rename its keyfile also - * @return bool Result of rename() - * @note This is pre rather than post because using post didn't work + * @param $path + * @return bool */ - public function preRename( $oldPath, $newPath ) { - - // Disable encryption proxy to prevent recursive calls - \OC_FileProxy::$enabled = false; - - $view = new \OC_FilesystemView( '/' ); - - $userId = \OCP\USER::getUser(); - - // Format paths to be relative to user files dir - $oldTrimmed = ltrim( $oldPath, '/' ); - $oldSplit = explode( '/', $oldTrimmed ); - $oldSliced = array_slice( $oldSplit, 2 ); - $oldRelPath = implode( '/', $oldSliced ); - $oldKeyfilePath = $userId . '/' . 'files_encryption' . '/' . 'keyfiles' . '/' . $oldRelPath . '.key'; - - $newTrimmed = ltrim( $newPath, '/' ); - $newSplit = explode( '/', $newTrimmed ); - $newSliced = array_slice( $newSplit, 2 ); - $newRelPath = implode( '/', $newSliced ); - $newKeyfilePath = $userId . '/' . 'files_encryption' . '/' . 'keyfiles' . '/' . $newRelPath . '.key'; - - // Rename keyfile so it isn't orphaned - $result = $view->rename( $oldKeyfilePath, $newKeyfilePath ); - - \OC_FileProxy::$enabled = true; - - return $result; - + public function postTouch( $path ) { + $this->handleFile( $path ); + + return true; } - - public function postFopen( $path, &$result ){ - + + /** + * @param $path + * @param $result + * @return resource + */ + public function postFopen( $path, &$result ) { + if ( !$result ) { - + return $result; - + } - + // Reformat path for use with OC_FSV $path_split = explode( '/', $path ); - $path_f = implode( array_slice( $path_split, 3 ) ); - + $path_f = implode( '/', array_slice( $path_split, 3 ) ); + + // FIXME: handling for /userId/cache used by webdav for chunking. The cache chunks are NOT encrypted + if ( count($path_split) >= 2 && $path_split[2] == 'cache' ) { + return $result; + } + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - + $meta = stream_get_meta_data( $result ); - + $view = new \OC_FilesystemView( '' ); - - $util = new Util( $view, \OCP\USER::getUser()); - + + $util = new Util( $view, \OCP\USER::getUser() ); + // If file is already encrypted, decrypt using crypto protocol - if ( - Crypt::mode() == 'server' - && $util->isEncryptedPath( $path ) + if ( + Crypt::mode() == 'server' + && $util->isEncryptedPath( $path ) ) { - + // Close the original encrypted file fclose( $result ); - + // Open the file using the crypto stream wrapper // protocol and let it do the decryption work instead $result = fopen( 'crypt://' . $path_f, $meta['mode'] ); - - - } elseif ( - self::shouldEncrypt( $path ) - and $meta ['mode'] != 'r' - and $meta['mode'] != 'rb' + + } elseif ( + self::shouldEncrypt( $path ) + and $meta ['mode'] != 'r' + and $meta['mode'] != 'rb' ) { - // If the file is not yet encrypted, but should be - // encrypted when it's saved (it's not read only) - - // NOTE: this is the case for new files saved via WebDAV - - if ( - $view->file_exists( $path ) - and $view->filesize( $path ) > 0 - ) { - $x = $view->file_get_contents( $path ); - - $tmp = tmpfile(); - -// // Make a temporary copy of the original file -// \OCP\Files::streamCopy( $result, $tmp ); -// -// // Close the original stream, we'll return another one -// fclose( $result ); -// -// $view->file_put_contents( $path_f, $tmp ); -// -// fclose( $tmp ); - - } - - $result = fopen( 'crypt://'.$path_f, $meta['mode'] ); - + $result = fopen( 'crypt://' . $path_f, $meta['mode'] ); } - + // Re-enable the proxy - \OC_FileProxy::$enabled = true; - + \OC_FileProxy::$enabled = $proxyStatus; + return $result; - - } - public function postGetMimeType( $path, $mime ) { - - if ( Crypt::isCatfile( $path ) ) { - - $mime = \OCP\Files::getMimeType( 'crypt://' . $path, 'w' ); - - } - - return $mime; - } - public function postStat( $path, $data ) { - - if ( Crypt::isCatfile( $path ) ) { - - $cached = \OC\Files\Filesystem::getFileInfo( $path, '' ); - - $data['size'] = $cached['size']; - + /** + * @param $path + * @param $data + * @return array + */ + public function postGetFileInfo( $path, $data ) { + + // if path is a folder do nothing + if ( is_array( $data ) && array_key_exists( 'size', $data ) ) { + + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + // get file size + $data['size'] = self::postFileSize( $path, $data['size'] ); + + // Re-enable the proxy + \OC_FileProxy::$enabled = $proxyStatus; } - + return $data; } + /** + * @param $path + * @param $size + * @return bool + */ public function postFileSize( $path, $size ) { - - if ( Crypt::isCatfile( $path ) ) { - - $cached = \OC\Files\Filesystem::getFileInfo( $path, '' ); - - return $cached['size']; - - } else { - + + $view = new \OC_FilesystemView( '/' ); + + // if path is a folder do nothing + if ( $view->is_dir( $path ) ) { + return $size; + } + + // Reformat path for use with OC_FSV + $path_split = explode( '/', $path ); + $path_f = implode( '/', array_slice( $path_split, 3 ) ); + + // if path is empty we cannot resolve anything + if ( empty( $path_f ) ) { return $size; - } + + $fileInfo = false; + // get file info from database/cache if not .part file + if ( !Keymanager::isPartialFilePath( $path ) ) { + $fileInfo = $view->getFileInfo( $path ); + } + + // if file is encrypted return real file size + if ( is_array( $fileInfo ) && $fileInfo['encrypted'] === true ) { + $size = $fileInfo['unencrypted_size']; + } else { + // self healing if file was removed from file cache + if ( !is_array( $fileInfo ) ) { + $fileInfo = array(); + } + + $userId = \OCP\User::getUser(); + $util = new Util( $view, $userId ); + $fixSize = $util->getFileSize( $path ); + if ( $fixSize > 0 ) { + $size = $fixSize; + + $fileInfo['encrypted'] = true; + $fileInfo['unencrypted_size'] = $size; + + // put file info if not .part file + if ( !Keymanager::isPartialFilePath( $path_f ) ) { + $view->putFileInfo( $path, $fileInfo ); + } + } + + } + return $size; + } + + /** + * @param $path + */ + public function handleFile( $path ) { + + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + $view = new \OC_FilesystemView( '/' ); + $session = new Session( $view ); + $userId = \OCP\User::getUser(); + $util = new Util( $view, $userId ); + + // Reformat path for use with OC_FSV + $path_split = explode( '/', $path ); + $path_f = implode( '/', array_slice( $path_split, 3 ) ); + + // only if file is on 'files' folder fix file size and sharing + if ( count($path_split) >= 2 && $path_split[2] == 'files' && $util->fixFileSize( $path ) ) { + + // get sharing app state + $sharingEnabled = \OCP\Share::isEnabled(); + + // get users + $usersSharing = $util->getSharingUsersArray( $sharingEnabled, $path_f ); + + // update sharing-keys + $util->setSharedFileKeyfiles( $session, $usersSharing, $path_f ); + } + + \OC_FileProxy::$enabled = $proxyStatus; } } diff --git a/apps/files_encryption/lib/session.php b/apps/files_encryption/lib/session.php index 769a40b359f..2ddad0a15da 100644 --- a/apps/files_encryption/lib/session.php +++ b/apps/files_encryption/lib/session.php @@ -26,78 +26,146 @@ namespace OCA\Encryption; * Class for handling encryption related session data */ -class Session { +class Session +{ + + private $view; + + /** + * @brief if session is started, check if ownCloud key pair is set up, if not create it + * @param \OC_FilesystemView $view + * + * @note The ownCloud key pair is used to allow public link sharing even if encryption is enabled + */ + public function __construct( $view ) { + + $this->view = $view; + + if ( !$this->view->is_dir( 'owncloud_private_key' ) ) { + + $this->view->mkdir( 'owncloud_private_key' ); + + } + + $publicShareKeyId = \OC_Appconfig::getValue( 'files_encryption', 'publicShareKeyId' ); + + if ( $publicShareKeyId === null ) { + $publicShareKeyId = 'pubShare_' . substr( md5( time() ), 0, 8 ); + \OC_Appconfig::setValue( 'files_encryption', 'publicShareKeyId', $publicShareKeyId ); + } + + if ( + !$this->view->file_exists( "/public-keys/" . $publicShareKeyId . ".public.key" ) + || !$this->view->file_exists( "/owncloud_private_key/" . $publicShareKeyId . ".private.key" ) + ) { + + $keypair = Crypt::createKeypair(); + + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + // Save public key + + if ( !$view->is_dir( '/public-keys' ) ) { + $view->mkdir( '/public-keys' ); + } + + $this->view->file_put_contents( '/public-keys/' . $publicShareKeyId . '.public.key', $keypair['publicKey'] ); + + // Encrypt private key empty passphrase + $encryptedPrivateKey = Crypt::symmetricEncryptFileContent( $keypair['privateKey'], '' ); + + // Save private key + $this->view->file_put_contents( '/owncloud_private_key/' . $publicShareKeyId . '.private.key', $encryptedPrivateKey ); + + \OC_FileProxy::$enabled = $proxyStatus; + + } + + if ( \OCP\USER::getUser() === false || + ( isset( $_GET['service'] ) && $_GET['service'] == 'files' && + isset( $_GET['t'] ) ) + ) { + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + $encryptedKey = $this->view->file_get_contents( '/owncloud_private_key/' . $publicShareKeyId . '.private.key' ); + $privateKey = Crypt::symmetricDecryptFileContent( $encryptedKey, '' ); + $this->setPrivateKey( $privateKey ); + + \OC_FileProxy::$enabled = $proxyStatus; + } + } /** * @brief Sets user private key to session + * @param string $privateKey * @return bool - * */ public function setPrivateKey( $privateKey ) { - + $_SESSION['privateKey'] = $privateKey; - + return true; - + } - + /** * @brief Gets user private key from session * @returns string $privateKey The user's plaintext private key * */ public function getPrivateKey() { - - if ( + + if ( isset( $_SESSION['privateKey'] ) && !empty( $_SESSION['privateKey'] ) ) { - + return $_SESSION['privateKey']; - + } else { - + return false; - + } - + } - + /** * @brief Sets user legacy key to session + * @param $legacyKey * @return bool - * */ public function setLegacyKey( $legacyKey ) { - - if ( $_SESSION['legacyKey'] = $legacyKey ) { - - return true; - - } - + + $_SESSION['legacyKey'] = $legacyKey; + + return true; } - + /** * @brief Gets user legacy key from session * @returns string $legacyKey The user's plaintext legacy key * */ public function getLegacyKey() { - - if ( + + if ( isset( $_SESSION['legacyKey'] ) && !empty( $_SESSION['legacyKey'] ) ) { - + return $_SESSION['legacyKey']; - + } else { - + return false; - + } - + } }
\ No newline at end of file diff --git a/apps/files_encryption/lib/stream.php b/apps/files_encryption/lib/stream.php index 65d7d57a05a..fa9df02f085 100644 --- a/apps/files_encryption/lib/stream.php +++ b/apps/files_encryption/lib/stream.php @@ -3,7 +3,7 @@ * ownCloud * * @author Robin Appelman - * @copyright 2012 Sam Tuke <samtuke@owncloud.com>, 2011 Robin Appelman + * @copyright 2012 Sam Tuke <samtuke@owncloud.com>, 2011 Robin Appelman * <icewind1991@gmail.com> * * This library is free software; you can redistribute it and/or @@ -32,27 +32,29 @@ namespace OCA\Encryption; /** * @brief Provides 'crypt://' stream wrapper protocol. - * @note We use a stream wrapper because it is the most secure way to handle + * @note We use a stream wrapper because it is the most secure way to handle * decrypted content transfers. There is no safe way to decrypt the entire file * somewhere on the server, so we have to encrypt and decrypt blocks on the fly. * @note Paths used with this protocol MUST BE RELATIVE. Use URLs like: - * crypt://filename, or crypt://subdirectory/filename, NOT - * crypt:///home/user/owncloud/data. Otherwise keyfiles will be put in - * [owncloud]/data/user/files_encryption/keyfiles/home/user/owncloud/data and + * crypt://filename, or crypt://subdirectory/filename, NOT + * crypt:///home/user/owncloud/data. Otherwise keyfiles will be put in + * [owncloud]/data/user/files_encryption/keyfiles/home/user/owncloud/data and * will not be accessible to other methods. - * @note Data read and written must always be 8192 bytes long, as this is the - * buffer size used internally by PHP. The encryption process makes the input - * data longer, and input is chunked into smaller pieces in order to result in + * @note Data read and written must always be 8192 bytes long, as this is the + * buffer size used internally by PHP. The encryption process makes the input + * data longer, and input is chunked into smaller pieces in order to result in * a 8192 encrypted block size. + * @note When files are deleted via webdav, or when they are updated and the + * previous version deleted, this is handled by OC\Files\View, and thus the + * encryption proxies are used and keyfiles deleted. */ -class Stream { +class Stream +{ + private $plainKey; + private $encKeyfiles; - public static $sourceStreams = array(); - - // TODO: make all below properties private again once unit testing is - // configured correctly - public $rawPath; // The raw path received by stream_open - public $path_f; // The raw path formatted to include username and data dir + private $rawPath; // The raw path relative to the data dir + private $relPath; // rel path to users file dir private $userId; private $handle; // Resource returned by fopen private $path; @@ -60,117 +62,99 @@ class Stream { private $meta = array(); // Header / meta for source stream private $count; private $writeCache; - public $size; + private $size; + private $unencryptedSize; private $publicKey; private $keyfile; private $encKeyfile; private static $view; // a fsview object set to user dir private $rootView; // a fsview object set to '/' + /** + * @param $path + * @param $mode + * @param $options + * @param $opened_path + * @return bool + */ public function stream_open( $path, $mode, $options, &$opened_path ) { - - // Get access to filesystem via filesystemview object - if ( !self::$view ) { - - self::$view = new \OC_FilesystemView( $this->userId . '/' ); + if ( !isset( $this->rootView ) ) { + $this->rootView = new \OC_FilesystemView( '/' ); } - - // Set rootview object if necessary - if ( ! $this->rootView ) { - $this->rootView = new \OC_FilesystemView( $this->userId . '/' ); + $util = new Util( $this->rootView, \OCP\USER::getUser() ); - } - - $this->userId = \OCP\User::getUser(); - - // Get the bare file path - $path = str_replace( 'crypt://', '', $path ); - - $this->rawPath = $path; - - $this->path_f = $this->userId . '/files/' . $path; - - if ( - dirname( $path ) == 'streams' - and isset( self::$sourceStreams[basename( $path )] ) - ) { - - // Is this just for unit testing purposes? - - $this->handle = self::$sourceStreams[basename( $path )]['stream']; + $this->userId = $util->getUserId(); - $this->path = self::$sourceStreams[basename( $path )]['path']; + // Strip identifier text from path, this gives us the path relative to data/<user>/files + $this->relPath = \OC\Files\Filesystem::normalizePath( str_replace( 'crypt://', '', $path ) ); - $this->size = self::$sourceStreams[basename( $path )]['size']; + // rawPath is relative to the data directory + $this->rawPath = $util->getUserFilesDir() . $this->relPath; - } else { + // Disable fileproxies so we can get the file size and open the source file without recursive encryption + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; - if ( - $mode == 'w' - or $mode == 'w+' - or $mode == 'wb' - or $mode == 'wb+' - ) { + if ( + $mode == 'w' + or $mode == 'w+' + or $mode == 'wb' + or $mode == 'wb+' + ) { - $this->size = 0; + // We're writing a new file so start write counter with 0 bytes + $this->size = 0; + $this->unencryptedSize = 0; - } else { - - - - $this->size = self::$view->filesize( $this->path_f, $mode ); - - //$this->size = filesize( $path ); - - } + } else { - // Disable fileproxies so we can open the source file without recursive encryption - \OC_FileProxy::$enabled = false; + $this->size = $this->rootView->filesize( $this->rawPath, $mode ); + } - //$this->handle = fopen( $path, $mode ); - - $this->handle = self::$view->fopen( $this->path_f, $mode ); - - \OC_FileProxy::$enabled = true; + $this->handle = $this->rootView->fopen( $this->rawPath, $mode ); - if ( !is_resource( $this->handle ) ) { + \OC_FileProxy::$enabled = $proxyStatus; - \OCP\Util::writeLog( 'files_encryption', 'failed to open '.$path, \OCP\Util::ERROR ); + if ( !is_resource( $this->handle ) ) { - } + \OCP\Util::writeLog( 'files_encryption', 'failed to open file "' . $this->rawPath . '"', \OCP\Util::ERROR ); - } - - if ( is_resource( $this->handle ) ) { + } else { $this->meta = stream_get_meta_data( $this->handle ); } + return is_resource( $this->handle ); } - + + /** + * @param $offset + * @param int $whence + */ public function stream_seek( $offset, $whence = SEEK_SET ) { - + $this->flush(); - + fseek( $this->handle, $offset, $whence ); - - } - - public function stream_tell() { - return ftell($this->handle); + } - + + /** + * @param $count + * @return bool|string + * @throws \Exception + */ public function stream_read( $count ) { - + $this->writeCache = ''; if ( $count != 8192 ) { - + // $count will always be 8192 https://bugs.php.net/bug.php?id=21641 // This makes this function a lot simpler, but will break this class if the above 'bug' gets 'fixed' \OCP\Util::writeLog( 'files_encryption', 'PHP "bug" 21641 no longer holds, decryption system requires refactoring', \OCP\Util::FATAL ); @@ -179,107 +163,89 @@ class Stream { } -// $pos = ftell( $this->handle ); -// // Get the data from the file handle $data = fread( $this->handle, 8192 ); - + + $result = ''; + if ( strlen( $data ) ) { - - $this->getKey(); - - $result = Crypt::symmetricDecryptFileContent( $data, $this->keyfile ); - - } else { - $result = ''; + if ( !$this->getKey() ) { - } + // Error! We don't have a key to decrypt the file with + throw new \Exception( 'Encryption key not found for "' . $this->rawPath . '" during attempted read via stream' ); -// $length = $this->size - $pos; -// -// if ( $length < 8192 ) { -// -// $result = substr( $result, 0, $length ); -// -// } + } + + // Decrypt data + $result = Crypt::symmetricDecryptFileContent( $data, $this->plainKey ); + + } return $result; } - + /** * @brief Encrypt and pad data ready for writing to disk * @param string $plainData data to be encrypted * @param string $key key to use for encryption - * @return encrypted data on success, false on failure + * @return string encrypted data on success, false on failure */ public function preWriteEncrypt( $plainData, $key ) { - + // Encrypt data to 'catfile', which includes IV if ( $encrypted = Crypt::symmetricEncryptFileContent( $plainData, $key ) ) { - - return $encrypted; - + + return $encrypted; + } else { - + return false; - + } - + } - + /** - * @brief Get the keyfile for the current file, generate one if necessary - * @param bool $generate if true, a new key will be generated if none can be found + * @brief Fetch the plain encryption key for the file and set it as plainKey property + * @internal param bool $generate if true, a new key will be generated if none can be found * @return bool true on key found and set, false on key not found and new key generated and set */ public function getKey() { - - // If a keyfile already exists for a file named identically to - // file to be written - if ( self::$view->file_exists( $this->userId . '/'. 'files_encryption' . '/' . 'keyfiles' . '/' . $this->rawPath . '.key' ) ) { - - // TODO: add error handling for when file exists but no - // keyfile - - // Fetch existing keyfile - $this->encKeyfile = Keymanager::getFileKey( $this->rootView, $this->userId, $this->rawPath ); - - $this->getUser(); - - $session = new Session(); - + + // Check if key is already set + if ( isset( $this->plainKey ) && isset( $this->encKeyfile ) ) { + + return true; + + } + + // Fetch and decrypt keyfile + // Fetch existing keyfile + $this->encKeyfile = Keymanager::getFileKey( $this->rootView, $this->userId, $this->relPath ); + + // If a keyfile already exists + if ( $this->encKeyfile ) { + + $session = new Session( $this->rootView ); + $privateKey = $session->getPrivateKey( $this->userId ); - - $this->keyfile = Crypt::keyDecrypt( $this->encKeyfile, $privateKey ); - + + $shareKey = Keymanager::getShareKey( $this->rootView, $this->userId, $this->relPath ); + + $this->plainKey = Crypt::multiKeyDecrypt( $this->encKeyfile, $shareKey, $privateKey ); + return true; - + } else { - + return false; - - } - - } - - public function getuser() { - - // Only get the user again if it isn't already set - if ( empty( $this->userId ) ) { - - // TODO: Move this user call out of here - it belongs - // elsewhere - $this->userId = \OCP\User::getUser(); - + } - - // TODO: Add a method for getting the user in case OCP\User:: - // getUser() doesn't work (can that scenario ever occur?) - + } - + /** * @brief Handle plain data from the stream, and write it in 8192 byte blocks * @param string $data data to be written to disk @@ -290,98 +256,54 @@ class Stream { * @note PHP automatically updates the file pointer after writing data to reflect it's length. There is generally no need to update the poitner manually using fseek */ public function stream_write( $data ) { - + // Disable the file proxies so that encryption is not // automatically attempted when the file is written to disk - // we are handling that separately here and we don't want to // get into an infinite loop + $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - + // Get the length of the unencrypted data that we are handling $length = strlen( $data ); - - // So far this round, no data has been written - $written = 0; - - // Find out where we are up to in the writing of data to the + + // Find out where we are up to in the writing of data to the // file $pointer = ftell( $this->handle ); - - // Make sure the userId is set - $this->getuser(); - - // TODO: Check if file is shared, if so, use multiKeyEncrypt and - // save shareKeys in necessary user directories - + // Get / generate the keyfile for the file we're handling // If we're writing a new file (not overwriting an existing // one), save the newly generated keyfile - if ( ! $this->getKey() ) { - - $this->keyfile = Crypt::generateKey(); - - $this->publicKey = Keymanager::getPublicKey( $this->rootView, $this->userId ); - - $this->encKeyfile = Crypt::keyEncrypt( $this->keyfile, $this->publicKey ); - - $view = new \OC_FilesystemView( '/' ); - $userId = \OCP\User::getUser(); - - // Save the new encrypted file key - Keymanager::setFileKey( $view, $this->rawPath, $userId, $this->encKeyfile ); - + if ( !$this->getKey() ) { + + $this->plainKey = Crypt::generateKey(); + } // If extra data is left over from the last round, make sure it // is integrated into the next 6126 / 8192 block if ( $this->writeCache ) { - + // Concat writeCache to start of $data $data = $this->writeCache . $data; - - // Clear the write cache, ready for resuse - it has been + + // Clear the write cache, ready for reuse - it has been // flushed and its old contents processed $this->writeCache = ''; } -// -// // Make sure we always start on a block start - if ( 0 != ( $pointer % 8192 ) ) { - // if the current position of - // file indicator is not aligned to a 8192 byte block, fix it - // so that it is - -// fseek( $this->handle, - ( $pointer % 8192 ), SEEK_CUR ); -// -// $pointer = ftell( $this->handle ); -// -// $unencryptedNewBlock = fread( $this->handle, 8192 ); -// -// fseek( $this->handle, - ( $currentPos % 8192 ), SEEK_CUR ); -// -// $block = Crypt::symmetricDecryptFileContent( $unencryptedNewBlock, $this->keyfile ); -// -// $x = substr( $block, 0, $currentPos % 8192 ); -// -// $data = $x . $data; -// -// fseek( $this->handle, - ( $currentPos % 8192 ), SEEK_CUR ); -// - } -// $currentPos = ftell( $this->handle ); - -// // While there still remains somed data to be processed & written - while( strlen( $data ) > 0 ) { -// -// // Remaining length for this iteration, not of the -// // entire file (may be greater than 8192 bytes) -// $remainingLength = strlen( $data ); -// -// // If data remaining to be written is less than the -// // size of 1 6126 byte block - if ( strlen( $data ) < 6126 ) { - + // While there still remains some data to be processed & written + while ( strlen( $data ) > 0 ) { + + // Remaining length for this iteration, not of the + // entire file (may be greater than 8192 bytes) + $remainingLength = strlen( $data ); + + // If data remaining to be written is less than the + // size of 1 6126 byte block + if ( $remainingLength < 6126 ) { + // Set writeCache to contents of $data // The writeCache will be carried over to the // next write round, and added to the start of @@ -394,98 +316,164 @@ class Stream { // Clear $data ready for next round $data = ''; -// + } else { - + // Read the chunk from the start of $data $chunk = substr( $data, 0, 6126 ); - - $encrypted = $this->preWriteEncrypt( $chunk, $this->keyfile ); - + + $encrypted = $this->preWriteEncrypt( $chunk, $this->plainKey ); + // Write the data chunk to disk. This will be // attended to the last data chunk if the file // being handled totals more than 6126 bytes fwrite( $this->handle, $encrypted ); - - $writtenLen = strlen( $encrypted ); - //fseek( $this->handle, $writtenLen, SEEK_CUR ); - // Remove the chunk we just processed from + // Remove the chunk we just processed from // $data, leaving only unprocessed data in $data // var, for handling on the next round $data = substr( $data, 6126 ); } - + } $this->size = max( $this->size, $pointer + $length ); - + $this->unencryptedSize += $length; + + \OC_FileProxy::$enabled = $proxyStatus; + return $length; } + /** + * @param $option + * @param $arg1 + * @param $arg2 + */ public function stream_set_option( $option, $arg1, $arg2 ) { - switch($option) { + $return = false; + switch ( $option ) { case STREAM_OPTION_BLOCKING: - stream_set_blocking( $this->handle, $arg1 ); + $return = stream_set_blocking( $this->handle, $arg1 ); break; case STREAM_OPTION_READ_TIMEOUT: - stream_set_timeout( $this->handle, $arg1, $arg2 ); + $return = stream_set_timeout( $this->handle, $arg1, $arg2 ); break; case STREAM_OPTION_WRITE_BUFFER: - stream_set_write_buffer( $this->handle, $arg1, $arg2 ); + $return = stream_set_write_buffer( $this->handle, $arg1 ); } + + return $return; } + /** + * @return array + */ public function stream_stat() { - return fstat($this->handle); + return fstat( $this->handle ); } - + + /** + * @param $mode + */ public function stream_lock( $mode ) { - flock( $this->handle, $mode ); + return flock( $this->handle, $mode ); } - + + /** + * @return bool + */ public function stream_flush() { - - return fflush( $this->handle ); + + return fflush( $this->handle ); // Not a typo: http://php.net/manual/en/function.fflush.php - + } + /** + * @return bool + */ public function stream_eof() { - return feof($this->handle); + return feof( $this->handle ); } private function flush() { - + if ( $this->writeCache ) { - + // Set keyfile property for file in question $this->getKey(); - - $encrypted = $this->preWriteEncrypt( $this->writeCache, $this->keyfile ); - + + $encrypted = $this->preWriteEncrypt( $this->writeCache, $this->plainKey ); + fwrite( $this->handle, $encrypted ); - + $this->writeCache = ''; - + } - + } + /** + * @return bool + */ public function stream_close() { - + $this->flush(); - if ( - $this->meta['mode']!='r' - and $this->meta['mode']!='rb' + if ( + $this->meta['mode'] != 'r' + and $this->meta['mode'] != 'rb' + and $this->size > 0 ) { + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + // Fetch user's public key + $this->publicKey = Keymanager::getPublicKey( $this->rootView, $this->userId ); + + // Check if OC sharing api is enabled + $sharingEnabled = \OCP\Share::isEnabled(); + + $util = new Util( $this->rootView, $this->userId ); + + // Get all users sharing the file includes current user + $uniqueUserIds = $util->getSharingUsersArray( $sharingEnabled, $this->relPath, $this->userId ); + + // Fetch public keys for all sharing users + $publicKeys = Keymanager::getPublicKeys( $this->rootView, $uniqueUserIds ); + + // Encrypt enc key for all sharing users + $this->encKeyfiles = Crypt::multiKeyEncrypt( $this->plainKey, $publicKeys ); + + $view = new \OC_FilesystemView( '/' ); + + // Save the new encrypted file key + Keymanager::setFileKey( $this->rootView, $this->relPath, $this->userId, $this->encKeyfiles['data'] ); + + // Save the sharekeys + Keymanager::setShareKeys( $view, $this->relPath, $this->encKeyfiles['keys'] ); + + // get file info + $fileInfo = $view->getFileInfo( $this->rawPath ); + if ( !is_array( $fileInfo ) ) { + $fileInfo = array(); + } + + // Re-enable proxy - our work is done + \OC_FileProxy::$enabled = $proxyStatus; - \OC\Files\Filesystem::putFileInfo( $this->path, array( 'encrypted' => true, 'size' => $this->size ), '' ); + // set encryption data + $fileInfo['encrypted'] = true; + $fileInfo['size'] = $this->size; + $fileInfo['unencrypted_size'] = $this->unencryptedSize; + // set fileinfo + $view->putFileInfo( $this->rawPath, $fileInfo ); } return fclose( $this->handle ); diff --git a/apps/files_encryption/lib/util.php b/apps/files_encryption/lib/util.php index 52bc74db27a..2980aa94e0c 100644 --- a/apps/files_encryption/lib/util.php +++ b/apps/files_encryption/lib/util.php @@ -3,8 +3,8 @@ * ownCloud * * @author Sam Tuke, Frank Karlitschek - * @copyright 2012 Sam Tuke samtuke@owncloud.com, - * Frank Karlitschek frank@owncloud.org + * @copyright 2012 Sam Tuke <samtuke@owncloud.com>, + * Frank Karlitschek <frank@owncloud.org> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE @@ -21,17 +21,29 @@ * */ -// Todo: +# Bugs +# ---- +# Sharing a file to a user without encryption set up will not provide them with access but won't notify the sharer +# Sharing all files to admin for recovery purposes still in progress +# Possibly public links are broken (not tested since last merge of master) + + +# Missing features +# ---------------- +# Make sure user knows if large files weren't encrypted + + +# Test +# ---- +# Test that writing files works when recovery is enabled, and sharing API is disabled +# Test trashbin support + + +// Old Todo: // - Crypt/decrypt button in the userinterface // - Setting if crypto should be on by default // - Add a setting "Don´t encrypt files larger than xx because of performance // reasons" -// - Transparent decrypt/encrypt in filesystem.php. Autodetect if a file is -// encrypted (.encrypted extension) -// - Don't use a password directly as encryption key. but a key which is -// stored on the server and encrypted with the user password. -> password -// change faster -// - IMPORTANT! Check if the block lenght of the encrypted data stays the same namespace OCA\Encryption; @@ -43,56 +55,49 @@ namespace OCA\Encryption; * unused, likely to become obsolete shortly */ -class Util { - - +class Util +{ + // Web UI: - + //// DONE: files created via web ui are encrypted //// DONE: file created & encrypted via web ui are readable in web ui //// DONE: file created & encrypted via web ui are readable via webdav - - + + // WebDAV: - + //// DONE: new data filled files added via webdav get encrypted //// DONE: new data filled files added via webdav are readable via webdav //// DONE: reading unencrypted files when encryption is enabled works via //// webdav //// DONE: files created & encrypted via web ui are readable via webdav - - + + // Legacy support: - + //// DONE: add method to check if file is encrypted using new system //// DONE: add method to check if file is encrypted using old system //// DONE: add method to fetch legacy key //// DONE: add method to decrypt legacy encrypted data - - + + // Admin UI: - + //// DONE: changing user password also changes encryption passphrase - + //// TODO: add support for optional recovery in case of lost passphrase / keys //// TODO: add admin optional required long passphrase for users - //// TODO: add UI buttons for encrypt / decrypt everything //// TODO: implement flag system to allow user to specify encryption by folder, subfolder, etc. - - - // Sharing: - - //// TODO: add support for encrypting to multiple public keys - //// TODO: add support for decrypting to multiple private keys - - + + // Integration testing: - + //// TODO: test new encryption with versioning - //// TODO: test new encryption with sharing + //// DONE: test new encryption with sharing //// TODO: test new encryption with proxies - - + + private $view; // OC_FilesystemView object for filesystem operations private $userId; // ID of the currently logged-in user private $pwd; // User Password @@ -103,166 +108,303 @@ class Util { private $shareKeysPath; // Dir containing env keys for shared files private $publicKeyPath; // Path to user's public key private $privateKeyPath; // Path to user's private key + private $publicShareKeyId; + private $recoveryKeyId; + private $isPublic; + /** + * @param \OC_FilesystemView $view + * @param $userId + * @param bool $client + */ public function __construct( \OC_FilesystemView $view, $userId, $client = false ) { - + $this->view = $view; $this->userId = $userId; $this->client = $client; - $this->userDir = '/' . $this->userId; - $this->userFilesDir = '/' . $this->userId . '/' . 'files'; - $this->publicKeyDir = '/' . 'public-keys'; - $this->encryptionDir = '/' . $this->userId . '/' . 'files_encryption'; - $this->keyfilesPath = $this->encryptionDir . '/' . 'keyfiles'; - $this->shareKeysPath = $this->encryptionDir . '/' . 'share-keys'; - $this->publicKeyPath = $this->publicKeyDir . '/' . $this->userId . '.public.key'; // e.g. data/public-keys/admin.public.key - $this->privateKeyPath = $this->encryptionDir . '/' . $this->userId . '.private.key'; // e.g. data/admin/admin.private.key - - } - + $this->isPublic = false; + + $this->publicShareKeyId = \OC_Appconfig::getValue( 'files_encryption', 'publicShareKeyId' ); + $this->recoveryKeyId = \OC_Appconfig::getValue( 'files_encryption', 'recoveryKeyId' ); + + // if we are anonymous/public + if ( $this->userId === false || + ( isset( $_GET['service'] ) && $_GET['service'] == 'files' && + isset( $_GET['t'] ) ) + ) { + $this->userId = $this->publicShareKeyId; + + // only handle for files_sharing app + if ( $GLOBALS['app'] === 'files_sharing' ) { + $this->userDir = '/' . $GLOBALS['fileOwner']; + $this->fileFolderName = 'files'; + $this->userFilesDir = '/' . $GLOBALS['fileOwner'] . '/' . $this->fileFolderName; // TODO: Does this need to be user configurable? + $this->publicKeyDir = '/' . 'public-keys'; + $this->encryptionDir = '/' . $GLOBALS['fileOwner'] . '/' . 'files_encryption'; + $this->keyfilesPath = $this->encryptionDir . '/' . 'keyfiles'; + $this->shareKeysPath = $this->encryptionDir . '/' . 'share-keys'; + $this->publicKeyPath = $this->publicKeyDir . '/' . $this->userId . '.public.key'; // e.g. data/public-keys/admin.public.key + $this->privateKeyPath = '/owncloud_private_key/' . $this->userId . '.private.key'; // e.g. data/admin/admin.private.key + $this->isPublic = true; + } + + } else { + $this->userDir = '/' . $this->userId; + $this->fileFolderName = 'files'; + $this->userFilesDir = '/' . $this->userId . '/' . $this->fileFolderName; // TODO: Does this need to be user configurable? + $this->publicKeyDir = '/' . 'public-keys'; + $this->encryptionDir = '/' . $this->userId . '/' . 'files_encryption'; + $this->keyfilesPath = $this->encryptionDir . '/' . 'keyfiles'; + $this->shareKeysPath = $this->encryptionDir . '/' . 'share-keys'; + $this->publicKeyPath = $this->publicKeyDir . '/' . $this->userId . '.public.key'; // e.g. data/public-keys/admin.public.key + $this->privateKeyPath = $this->encryptionDir . '/' . $this->userId . '.private.key'; // e.g. data/admin/admin.private.key + } + } + + /** + * @return bool + */ public function ready() { - - if( - !$this->view->file_exists( $this->encryptionDir ) - or !$this->view->file_exists( $this->keyfilesPath ) - or !$this->view->file_exists( $this->shareKeysPath ) - or !$this->view->file_exists( $this->publicKeyPath ) - or !$this->view->file_exists( $this->privateKeyPath ) + + if ( + !$this->view->file_exists( $this->encryptionDir ) + or !$this->view->file_exists( $this->keyfilesPath ) + or !$this->view->file_exists( $this->shareKeysPath ) + or !$this->view->file_exists( $this->publicKeyPath ) + or !$this->view->file_exists( $this->privateKeyPath ) ) { - + return false; - + } else { - + return true; - + } - + } - - /** - * @brief Sets up user folders and keys for serverside encryption - * @param $passphrase passphrase to encrypt server-stored private key with - */ + + /** + * @brief Sets up user folders and keys for serverside encryption + * @param string $passphrase passphrase to encrypt server-stored private key with + */ public function setupServerSide( $passphrase = null ) { - - // Create user dir - if( !$this->view->file_exists( $this->userDir ) ) { - - $this->view->mkdir( $this->userDir ); - - } - - // Create user files dir - if( !$this->view->file_exists( $this->userFilesDir ) ) { - - $this->view->mkdir( $this->userFilesDir ); - - } - - // Create shared public key directory - if( !$this->view->file_exists( $this->publicKeyDir ) ) { - - $this->view->mkdir( $this->publicKeyDir ); - - } - - // Create encryption app directory - if( !$this->view->file_exists( $this->encryptionDir ) ) { - - $this->view->mkdir( $this->encryptionDir ); - - } - - // Create mirrored keyfile directory - if( !$this->view->file_exists( $this->keyfilesPath ) ) { - - $this->view->mkdir( $this->keyfilesPath ); - - } - - // Create mirrored share env keys directory - if( !$this->view->file_exists( $this->shareKeysPath ) ) { - - $this->view->mkdir( $this->shareKeysPath ); - - } - + + // Set directories to check / create + $setUpDirs = array( + $this->userDir + , $this->userFilesDir + , $this->publicKeyDir + , $this->encryptionDir + , $this->keyfilesPath + , $this->shareKeysPath + ); + + // Check / create all necessary dirs + foreach ( $setUpDirs as $dirPath ) { + + if ( !$this->view->file_exists( $dirPath ) ) { + + $this->view->mkdir( $dirPath ); + + } + + } + // Create user keypair - if ( - ! $this->view->file_exists( $this->publicKeyPath ) - or ! $this->view->file_exists( $this->privateKeyPath ) + // we should never override a keyfile + if ( + !$this->view->file_exists( $this->publicKeyPath ) + && !$this->view->file_exists( $this->privateKeyPath ) ) { - + // Generate keypair $keypair = Crypt::createKeypair(); - + \OC_FileProxy::$enabled = false; - + // Save public key $this->view->file_put_contents( $this->publicKeyPath, $keypair['publicKey'] ); - + // Encrypt private key with user pwd as passphrase $encryptedPrivateKey = Crypt::symmetricEncryptFileContent( $keypair['privateKey'], $passphrase ); - + // Save private key $this->view->file_put_contents( $this->privateKeyPath, $encryptedPrivateKey ); - + \OC_FileProxy::$enabled = true; - + + } else { + // check if public-key exists but private-key is missing + if ( $this->view->file_exists( $this->publicKeyPath ) && !$this->view->file_exists( $this->privateKeyPath ) ) { + \OC_Log::write( 'Encryption library', 'public key exists but private key is missing for "' . $this->userId . '"', \OC_Log::FATAL ); + return false; + } else if ( !$this->view->file_exists( $this->publicKeyPath ) && $this->view->file_exists( $this->privateKeyPath ) ) { + \OC_Log::write( 'Encryption library', 'private key exists but public key is missing for "' . $this->userId . '"', \OC_Log::FATAL ); + return false; + } } - + + // If there's no record for this user's encryption preferences + if ( false === $this->recoveryEnabledForUser() ) { + + // create database configuration + $sql = 'INSERT INTO `*PREFIX*encryption` (`uid`,`mode`,`recovery_enabled`) VALUES (?,?,?)'; + $args = array( $this->userId, 'server-side', 0 ); + $query = \OCP\DB::prepare( $sql ); + $query->execute( $args ); + + } + return true; - + } - + + /** + * @return string + */ + public function getPublicShareKeyId() { + return $this->publicShareKeyId; + } + + /** + * @brief Check whether pwd recovery is enabled for a given user + * @return bool 1 = yes, 0 = no, false = no record + * + * @note If records are not being returned, check for a hidden space + * at the start of the uid in db + */ + public function recoveryEnabledForUser() { + + $sql = 'SELECT + recovery_enabled + FROM + `*PREFIX*encryption` + WHERE + uid = ?'; + + $args = array( $this->userId ); + + $query = \OCP\DB::prepare( $sql ); + + $result = $query->execute( $args ); + + $recoveryEnabled = array(); + + while ( $row = $result->fetchRow() ) { + + $recoveryEnabled[] = $row['recovery_enabled']; + + } + + // If no record is found + if ( empty( $recoveryEnabled ) ) { + + return false; + + // If a record is found + } else { + + return $recoveryEnabled[0]; + + } + + } + + /** + * @brief Enable / disable pwd recovery for a given user + * @param bool $enabled Whether to enable or disable recovery + * @return bool + */ + public function setRecoveryForUser( $enabled ) { + + $recoveryStatus = $this->recoveryEnabledForUser(); + + // If a record for this user already exists, update it + if ( false === $recoveryStatus ) { + + $sql = 'INSERT INTO `*PREFIX*encryption` + (`uid`,`mode`,`recovery_enabled`) + VALUES (?,?,?)'; + + $args = array( $this->userId, 'server-side', $enabled ); + + // Create a new record instead + } else { + + $sql = 'UPDATE + *PREFIX*encryption + SET + recovery_enabled = ? + WHERE + uid = ?'; + + $args = array( $enabled, $this->userId ); + + } + + $query = \OCP\DB::prepare( $sql ); + + if ( $query->execute( $args ) ) { + + return true; + + } else { + + return false; + + } + + } + /** * @brief Find all files and their encryption status within a directory * @param string $directory The path of the parent directory to search * @return mixed false if 0 found, array on success. Keys: name, path - * @note $directory needs to be a path relative to OC data dir. e.g. * /admin/files NOT /backup OR /home/www/oc/data/admin/files */ - public function findFiles( $directory ) { - + public function findEncFiles( $directory, &$found = false ) { + // Disable proxy - we don't want files to be decrypted before // we handle them \OC_FileProxy::$enabled = false; - - $found = array( 'plain' => array(), 'encrypted' => array(), 'legacy' => array() ); - - if ( - $this->view->is_dir( $directory ) - && $handle = $this->view->opendir( $directory ) + + if ( $found == false ) { + $found = array( 'plain' => array(), 'encrypted' => array(), 'legacy' => array() ); + } + + if ( + $this->view->is_dir( $directory ) + && $handle = $this->view->opendir( $directory ) ) { - + while ( false !== ( $file = readdir( $handle ) ) ) { - + if ( - $file != "." - && $file != ".." + $file != "." + && $file != ".." ) { - + $filePath = $directory . '/' . $this->view->getRelativePath( '/' . $file ); $relPath = $this->stripUserFilesPath( $filePath ); - + // If the path is a directory, search // its contents - if ( $this->view->is_dir( $filePath ) ) { - - $this->findFiles( $filePath ); - - // If the path is a file, determine - // its encryption status + if ( $this->view->is_dir( $filePath ) ) { + + $this->findEncFiles( $filePath, $found ); + + // If the path is a file, determine + // its encryption status } elseif ( $this->view->is_file( $filePath ) ) { - + // Disable proxies again, some- // where they got re-enabled :/ \OC_FileProxy::$enabled = false; - + $data = $this->view->file_get_contents( $filePath ); - + // If the file is encrypted // NOTE: If the userId is // empty or not set, file will @@ -270,207 +412,1049 @@ class Util { // NOTE: This is inefficient; // scanning every file like this // will eat server resources :( - if ( - Keymanager::getFileKey( $this->view, $this->userId, $file ) - && Crypt::isCatfile( $data ) + if ( + Keymanager::getFileKey( $this->view, $this->userId, $relPath ) + && Crypt::isCatfileContent( $data ) ) { - + $found['encrypted'][] = array( 'name' => $file, 'path' => $filePath ); - - // If the file uses old - // encryption system - } elseif ( Crypt::isLegacyEncryptedContent( $this->view->file_get_contents( $filePath ), $relPath ) ) { - + + // If the file uses old + // encryption system + } elseif ( Crypt::isLegacyEncryptedContent( $this->tail( $filePath, 3 ), $relPath ) ) { + $found['legacy'][] = array( 'name' => $file, 'path' => $filePath ); - - // If the file is not encrypted + + // If the file is not encrypted } else { - - $found['plain'][] = array( 'name' => $file, 'path' => $filePath ); - + + $found['plain'][] = array( 'name' => $file, 'path' => $relPath ); + } - + } - + } - + } - + \OC_FileProxy::$enabled = true; - + if ( empty( $found ) ) { - + return false; - + } else { - + return $found; - + } - + } - + \OC_FileProxy::$enabled = true; - + return false; } - - /** - * @brief Check if a given path identifies an encrypted file - * @return true / false - */ + + /** + * @brief Fetch the last lines of a file efficiently + * @note Safe to use on large files; does not read entire file to memory + * @note Derivative of http://tekkie.flashbit.net/php/tail-functionality-in-php + */ + public function tail( $filename, $numLines ) { + + \OC_FileProxy::$enabled = false; + + $text = ''; + $pos = -1; + $handle = $this->view->fopen( $filename, 'r' ); + + while ( $numLines > 0 ) { + + --$pos; + + if ( fseek( $handle, $pos, SEEK_END ) !== 0 ) { + + rewind( $handle ); + $numLines = 0; + + } elseif ( fgetc( $handle ) === "\n" ) { + + --$numLines; + + } + + $block_size = ( -$pos ) % 8192; + if ( $block_size === 0 || $numLines === 0 ) { + + $text = fread( $handle, ( $block_size === 0 ? 8192 : $block_size ) ) . $text; + + } + } + + fclose( $handle ); + + \OC_FileProxy::$enabled = true; + + return $text; + } + + /** + * @brief Check if a given path identifies an encrypted file + * @param $path + * @return boolean + */ public function isEncryptedPath( $path ) { - - // Disable encryption proxy so data retreived is in its + + // Disable encryption proxy so data retrieved is in its // original form + $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - - $data = $this->view->file_get_contents( $path ); - - \OC_FileProxy::$enabled = true; - - return Crypt::isCatfile( $data ); - + + // we only need 24 byte from the last chunk + $data = ''; + $handle = $this->view->fopen( $path, 'r' ); + if ( !fseek( $handle, -24, SEEK_END ) ) { + $data = fgets( $handle ); + } + + // re-enable proxy + \OC_FileProxy::$enabled = $proxyStatus; + + return Crypt::isCatfileContent( $data ); + } - + + /** + * @brief get the file size of the unencrypted file + * @param string $path absolute path + * @return bool + */ + public function getFileSize( $path ) { + + $result = 0; + + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + // Reformat path for use with OC_FSV + $pathSplit = explode( '/', $path ); + $pathRelative = implode( '/', array_slice( $pathSplit, 3 ) ); + + if ( $pathSplit[2] == 'files' && $this->view->file_exists( $path ) && $this->isEncryptedPath( $path ) ) { + + // get the size from filesystem + $fullPath = $this->view->getLocalFile( $path ); + $size = filesize( $fullPath ); + + // calculate last chunk nr + $lastChunkNr = floor( $size / 8192 ); + + // open stream + $stream = fopen( 'crypt://' . $pathRelative, "r" ); + + if ( is_resource( $stream ) ) { + // calculate last chunk position + $lastChunckPos = ( $lastChunkNr * 8192 ); + + // seek to end + fseek( $stream, $lastChunckPos ); + + // get the content of the last chunk + $lastChunkContent = fread( $stream, 8192 ); + + // calc the real file size with the size of the last chunk + $realSize = ( ( $lastChunkNr * 6126 ) + strlen( $lastChunkContent ) ); + + // store file size + $result = $realSize; + } + } + + \OC_FileProxy::$enabled = $proxyStatus; + + return $result; + } + + /** + * @brief fix the file size of the encrypted file + * @param $path absolute path + * @return true / false if file is encrypted + */ + public function fixFileSize( $path ) { + + $result = false; + + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + $realSize = $this->getFileSize( $path ); + + if ( $realSize > 0 ) { + + $cached = $this->view->getFileInfo( $path ); + $cached['encrypted'] = true; + + // set the size + $cached['unencrypted_size'] = $realSize; + + // put file info + $this->view->putFileInfo( $path, $cached ); + + $result = true; + + } + + \OC_FileProxy::$enabled = $proxyStatus; + + return $result; + } + /** * @brief Format a path to be relative to the /user/files/ directory + * @note e.g. turns '/admin/files/test.txt' into 'test.txt' */ public function stripUserFilesPath( $path ) { - + $trimmed = ltrim( $path, '/' ); $split = explode( '/', $trimmed ); $sliced = array_slice( $split, 2 ); $relPath = implode( '/', $sliced ); - + return $relPath; - + + } + + /** + * @param $path + * @return bool + */ + public function isSharedPath( $path ) { + + $trimmed = ltrim( $path, '/' ); + $split = explode( '/', $trimmed ); + + if ( $split[2] == "Shared" ) { + + return true; + + } else { + + return false; + + } + } - + /** * @brief Encrypt all files in a directory - * @param string $publicKey the public key to encrypt files with * @param string $dirPath the directory whose files will be encrypted + * @param null $legacyPassphrase + * @param null $newPassphrase + * @return bool * @note Encryption is recursive */ - public function encryptAll( $publicKey, $dirPath, $legacyPassphrase = null, $newPassphrase = null ) { - - if ( $found = $this->findFiles( $dirPath ) ) { - + public function encryptAll( $dirPath, $legacyPassphrase = null, $newPassphrase = null ) { + + if ( $found = $this->findEncFiles( $dirPath ) ) { + // Disable proxy to prevent file being encrypted twice \OC_FileProxy::$enabled = false; - + // Encrypt unencrypted files foreach ( $found['plain'] as $plainFile ) { - - // Fetch data from file - $plainData = $this->view->file_get_contents( $plainFile['path'] ); - - // Encrypt data, generate catfile - $encrypted = Crypt::keyEncryptKeyfile( $plainData, $publicKey ); - - $relPath = $this->stripUserFilesPath( $plainFile['path'] ); - - // Save keyfile - Keymanager::setFileKey( $this->view, $relPath, $this->userId, $encrypted['key'] ); - - // Overwrite the existing file with the encrypted one - $this->view->file_put_contents( $plainFile['path'], $encrypted['data'] ); - - $size = strlen( $encrypted['data'] ); - + + //relative to data/<user>/file + $relPath = $plainFile['path']; + + //relative to /data + $rawPath = $this->userId . '/files/' . $plainFile['path']; + + // Open plain file handle for binary reading + $plainHandle1 = $this->view->fopen( $rawPath, 'rb' ); + + // 2nd handle for moving plain file - view->rename() doesn't work, this is a workaround + $plainHandle2 = $this->view->fopen( $rawPath . '.plaintmp', 'wb' ); + + // Move plain file to a temporary location + stream_copy_to_stream( $plainHandle1, $plainHandle2 ); + + // Close access to original file + // $this->view->fclose( $plainHandle1 ); // not implemented in view{} + // Delete original plain file so we can rename enc file later + $this->view->unlink( $rawPath ); + + // Open enc file handle for binary writing, with same filename as original plain file + $encHandle = fopen( 'crypt://' . $relPath, 'wb' ); + + // Save data from plain stream to new encrypted file via enc stream + // NOTE: Stream{} will be invoked for handling + // the encryption, and should handle all keys + // and their generation etc. automatically + stream_copy_to_stream( $plainHandle2, $encHandle ); + + // get file size + $size = $this->view->filesize( $rawPath . '.plaintmp' ); + + // Delete temporary plain copy of file + $this->view->unlink( $rawPath . '.plaintmp' ); + // Add the file to the cache - \OC\Files\Filesystem::putFileInfo( $plainFile['path'], array( 'encrypted'=>true, 'size' => $size ), '' ); - + \OC\Files\Filesystem::putFileInfo( $plainFile['path'], array( 'encrypted' => true, 'size' => $size, 'unencrypted_size' => $size ) ); } - + // Encrypt legacy encrypted files - if ( - ! empty( $legacyPassphrase ) - && ! empty( $newPassphrase ) + if ( + !empty( $legacyPassphrase ) + && !empty( $newPassphrase ) ) { - + foreach ( $found['legacy'] as $legacyFile ) { - + // Fetch data from file $legacyData = $this->view->file_get_contents( $legacyFile['path'] ); - + + $sharingEnabled = \OCP\Share::isEnabled(); + + // if file exists try to get sharing users + if ( $this->view->file_exists( $legacyFile['path'] ) ) { + $uniqueUserIds = $this->getSharingUsersArray( $sharingEnabled, $legacyFile['path'], $this->userId ); + } else { + $uniqueUserIds[] = $this->userId; + } + + // Fetch public keys for all users who will share the file + $publicKeys = Keymanager::getPublicKeys( $this->view, $uniqueUserIds ); + // Recrypt data, generate catfile - $recrypted = Crypt::legacyKeyRecryptKeyfile( $legacyData, $legacyPassphrase, $publicKey, $newPassphrase ); - - $relPath = $this->stripUserFilesPath( $legacyFile['path'] ); - + $recrypted = Crypt::legacyKeyRecryptKeyfile( $legacyData, $legacyPassphrase, $publicKeys, $newPassphrase, $legacyFile['path'] ); + + $rawPath = $legacyFile['path']; + $relPath = $this->stripUserFilesPath( $rawPath ); + // Save keyfile - Keymanager::setFileKey( $this->view, $relPath, $this->userId, $recrypted['key'] ); - + Keymanager::setFileKey( $this->view, $relPath, $this->userId, $recrypted['filekey'] ); + + // Save sharekeys to user folders + Keymanager::setShareKeys( $this->view, $relPath, $recrypted['sharekeys'] ); + // Overwrite the existing file with the encrypted one - $this->view->file_put_contents( $legacyFile['path'], $recrypted['data'] ); - + $this->view->file_put_contents( $rawPath, $recrypted['data'] ); + $size = strlen( $recrypted['data'] ); - + // Add the file to the cache - \OC\Files\Filesystem::putFileInfo( $legacyFile['path'], array( 'encrypted'=>true, 'size' => $size ), '' ); - + \OC\Files\Filesystem::putFileInfo( $rawPath, array( 'encrypted' => true, 'size' => $size ), '' ); } - } - + \OC_FileProxy::$enabled = true; - + // If files were found, return true return true; - } else { - + // If no files were found, return false return false; - } - } - + /** * @brief Return important encryption related paths * @param string $pathName Name of the directory to return the path of * @return string path */ public function getPath( $pathName ) { - + switch ( $pathName ) { - + case 'publicKeyDir': - + return $this->publicKeyDir; - + break; - + case 'encryptionDir': - + return $this->encryptionDir; - + break; - + case 'keyfilesPath': - + return $this->keyfilesPath; - + break; - + case 'publicKeyPath': - + return $this->publicKeyPath; - + break; - + case 'privateKeyPath': - + return $this->privateKeyPath; - + break; - } - + + return false; + + } + + /** + * @brief get path of a file. + * @param int $fileId id of the file + * @return string path of the file + */ + public static function fileIdToPath( $fileId ) { + + $query = \OC_DB::prepare( 'SELECT `path`' + . ' FROM `*PREFIX*filecache`' + . ' WHERE `fileid` = ?' ); + + $result = $query->execute( array( $fileId ) ); + + $row = $result->fetchRow(); + + return substr( $row['path'], 5 ); + + } + + /** + * @brief Filter an array of UIDs to return only ones ready for sharing + * @param array $unfilteredUsers users to be checked for sharing readiness + * @return multi-dimensional array. keys: ready, unready + */ + public function filterShareReadyUsers( $unfilteredUsers ) { + + // This array will collect the filtered IDs + $readyIds = $unreadyIds = array(); + + // Loop through users and create array of UIDs that need new keyfiles + foreach ( $unfilteredUsers as $user ) { + + $util = new Util( $this->view, $user ); + + // Check that the user is encryption capable, or is the + // public system user 'ownCloud' (for public shares) + if ( + $user == $this->publicShareKeyId + or $user == $this->recoveryKeyId + or $util->ready() + ) { + + // Construct array of ready UIDs for Keymanager{} + $readyIds[] = $user; + + } else { + + // Construct array of unready UIDs for Keymanager{} + $unreadyIds[] = $user; + + // Log warning; we can't do necessary setup here + // because we don't have the user passphrase + \OC_Log::write( 'Encryption library', '"' . $user . '" is not setup for encryption', \OC_Log::WARN ); + + } + + } + + return array( + 'ready' => $readyIds, + 'unready' => $unreadyIds + ); + + } + + /** + * @brief Decrypt a keyfile without knowing how it was encrypted + * @param string $filePath + * @param string $fileOwner + * @param string $privateKey + * @note Checks whether file was encrypted with openssl_seal or + * openssl_encrypt, and decrypts accrdingly + * @note This was used when 2 types of encryption for keyfiles was used, + * but now we've switched to exclusively using openssl_seal() + */ + public function decryptUnknownKeyfile( $filePath, $fileOwner, $privateKey ) { + + // Get the encrypted keyfile + // NOTE: the keyfile format depends on how it was encrypted! At + // this stage we don't know how it was encrypted + $encKeyfile = Keymanager::getFileKey( $this->view, $this->userId, $filePath ); + + // We need to decrypt the keyfile + // Has the file been shared yet? + if ( + $this->userId == $fileOwner + && !Keymanager::getShareKey( $this->view, $this->userId, $filePath ) // NOTE: we can't use isShared() here because it's a post share hook so it always returns true + ) { + + // The file has no shareKey, and its keyfile must be + // decrypted conventionally + $plainKeyfile = Crypt::keyDecrypt( $encKeyfile, $privateKey ); + + + } else { + + // The file has a shareKey and must use it for decryption + $shareKey = Keymanager::getShareKey( $this->view, $this->userId, $filePath ); + + $plainKeyfile = Crypt::multiKeyDecrypt( $encKeyfile, $shareKey, $privateKey ); + + } + + return $plainKeyfile; + + } + + /** + * @brief Encrypt keyfile to multiple users + * @param Session $session + * @param array $users list of users which should be able to access the file + * @param string $filePath path of the file to be shared + * @return bool + */ + public function setSharedFileKeyfiles( Session $session, array $users, $filePath ) { + + // Make sure users are capable of sharing + $filteredUids = $this->filterShareReadyUsers( $users ); + + // If we're attempting to share to unready users + if ( !empty( $filteredUids['unready'] ) ) { + + \OC_Log::write( 'Encryption library', 'Sharing to these user(s) failed as they are unready for encryption:"' . print_r( $filteredUids['unready'], 1 ), \OC_Log::WARN ); + + return false; + + } + + // Get public keys for each user, ready for generating sharekeys + $userPubKeys = Keymanager::getPublicKeys( $this->view, $filteredUids['ready'] ); + + // Note proxy status then disable it + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + // Get the current users's private key for decrypting existing keyfile + $privateKey = $session->getPrivateKey(); + + $fileOwner = \OC\Files\Filesystem::getOwner( $filePath ); + + // Decrypt keyfile + $plainKeyfile = $this->decryptUnknownKeyfile( $filePath, $fileOwner, $privateKey ); + + // Re-enc keyfile to (additional) sharekeys + $multiEncKey = Crypt::multiKeyEncrypt( $plainKeyfile, $userPubKeys ); + + // Save the recrypted key to it's owner's keyfiles directory + // Save new sharekeys to all necessary user directory + if ( + !Keymanager::setFileKey( $this->view, $filePath, $fileOwner, $multiEncKey['data'] ) + || !Keymanager::setShareKeys( $this->view, $filePath, $multiEncKey['keys'] ) + ) { + + \OC_Log::write( 'Encryption library', 'Keyfiles could not be saved for users sharing ' . $filePath, \OC_Log::ERROR ); + + return false; + + } + + // Return proxy to original status + \OC_FileProxy::$enabled = $proxyStatus; + + return true; + } + + /** + * @brief Find, sanitise and format users sharing a file + * @note This wraps other methods into a portable bundle + */ + public function getSharingUsersArray( $sharingEnabled, $filePath, $currentUserId = false ) { + + // Check if key recovery is enabled + if ( + \OC_Appconfig::getValue( 'files_encryption', 'recoveryAdminEnabled' ) + && $this->recoveryEnabledForUser() + ) { + + $recoveryEnabled = true; + + } else { + + $recoveryEnabled = false; + + } + + // Make sure that a share key is generated for the owner too + list( $owner, $ownerPath ) = $this->getUidAndFilename( $filePath ); + + $userIds = array(); + if ( $sharingEnabled ) { + + // Find out who, if anyone, is sharing the file + $result = \OCP\Share::getUsersSharingFile( $ownerPath, $owner, true, true, true ); + $userIds = $result['users']; + if ( $result['public'] ) { + $userIds[] = $this->publicShareKeyId; + } + + } + + // If recovery is enabled, add the + // Admin UID to list of users to share to + if ( $recoveryEnabled ) { + + // Find recoveryAdmin user ID + $recoveryKeyId = \OC_Appconfig::getValue( 'files_encryption', 'recoveryKeyId' ); + + // Add recoveryAdmin to list of users sharing + $userIds[] = $recoveryKeyId; + + } + + // add current user if given + if ( $currentUserId != false ) { + + $userIds[] = $currentUserId; + + } + + // Remove duplicate UIDs + $uniqueUserIds = array_unique( $userIds ); + + return $uniqueUserIds; + + } + + /** + * @brief Set file migration status for user + * @param $status + * @return bool + */ + public function setMigrationStatus( $status ) { + + $sql = 'UPDATE + *PREFIX*encryption + SET + migration_status = ? + WHERE + uid = ?'; + + $args = array( $status, $this->userId ); + + $query = \OCP\DB::prepare( $sql ); + + if ( $query->execute( $args ) ) { + + return true; + + } else { + + return false; + + } + + } + + /** + * @brief Check whether pwd recovery is enabled for a given user + * @return bool 1 = yes, 0 = no, false = no record + * @note If records are not being returned, check for a hidden space + * at the start of the uid in db + */ + public function getMigrationStatus() { + + $sql = 'SELECT + migration_status + FROM + `*PREFIX*encryption` + WHERE + uid = ?'; + + $args = array( $this->userId ); + + $query = \OCP\DB::prepare( $sql ); + + $result = $query->execute( $args ); + + $migrationStatus = array(); + + $row = $result->fetchRow(); + if($row) { + $migrationStatus[] = $row['migration_status']; + } + + // If no record is found + if ( empty( $migrationStatus ) ) { + + return false; + + // If a record is found + } else { + + return $migrationStatus[0]; + + } + + } + + /** + * @brief get uid of the owners of the file and the path to the file + * @param string $path Path of the file to check + * @note $shareFilePath must be relative to data/UID/files. Files + * relative to /Shared are also acceptable + * @return array + */ + public function getUidAndFilename( $path ) { + + $view = new \OC\Files\View( $this->userFilesDir ); + $fileOwnerUid = $view->getOwner( $path ); + + // handle public access + if ( $this->isPublic ) { + $filename = $path; + $fileOwnerUid = $GLOBALS['fileOwner']; + + return array( $fileOwnerUid, $filename ); + } else { + + // Check that UID is valid + if ( !\OCP\User::userExists( $fileOwnerUid ) ) { + throw new \Exception( 'Could not find owner (UID = "' . var_export( $fileOwnerUid, 1 ) . '") of file "' . $path . '"' ); + } + + // NOTE: Bah, this dependency should be elsewhere + \OC\Files\Filesystem::initMountPoints( $fileOwnerUid ); + + // If the file owner is the currently logged in user + if ( $fileOwnerUid == $this->userId ) { + + // Assume the path supplied is correct + $filename = $path; + + } else { + + $info = $view->getFileInfo( $path ); + $ownerView = new \OC\Files\View( '/' . $fileOwnerUid . '/files' ); + + // Fetch real file path from DB + $filename = $ownerView->getPath( $info['fileid'] ); // TODO: Check that this returns a path without including the user data dir + + } + + return array( $fileOwnerUid, $filename ); + } + + + } + + /** + * @brief go recursively through a dir and collect all files and sub files. + * @param string $dir relative to the users files folder + * @return array with list of files relative to the users files folder + */ + public function getAllFiles( $dir ) { + + $result = array(); + + $content = $this->view->getDirectoryContent( $this->userFilesDir . $dir ); + + // handling for re shared folders + $path_split = explode( '/', $dir ); + + foreach ( $content as $c ) { + + $sharedPart = $path_split[sizeof( $path_split ) - 1]; + $targetPathSplit = array_reverse( explode( '/', $c['path'] ) ); + + $path = ''; + + // rebuild path + foreach ( $targetPathSplit as $pathPart ) { + + if ( $pathPart !== $sharedPart ) { + + $path = '/' . $pathPart . $path; + + } else { + + break; + + } + + } + + $path = $dir . $path; + + if ( $c['type'] === "dir" ) { + + $result = array_merge( $result, $this->getAllFiles( $path ) ); + + } else { + + $result[] = $path; + + } + } + + return $result; + + } + + /** + * @brief get shares parent. + * @param int $id of the current share + * @return array of the parent + */ + public static function getShareParent( $id ) { + + $query = \OC_DB::prepare( 'SELECT `file_target`, `item_type`' + . ' FROM `*PREFIX*share`' + . ' WHERE `id` = ?' ); + + $result = $query->execute( array( $id ) ); + + $row = $result->fetchRow(); + + return $row; + + } + + /** + * @brief get shares parent. + * @param int $id of the current share + * @return array of the parent + */ + public static function getParentFromShare( $id ) { + + $query = \OC_DB::prepare( 'SELECT `parent`' + . ' FROM `*PREFIX*share`' + . ' WHERE `id` = ?' ); + + $result = $query->execute( array( $id ) ); + + $row = $result->fetchRow(); + + return $row; + + } + + /** + * @brief get owner of the shared files. + * @param $id + * @internal param int $Id of a share + * @return string owner + */ + public function getOwnerFromSharedFile( $id ) { + + $query = \OC_DB::prepare( 'SELECT `parent`, `uid_owner` FROM `*PREFIX*share` WHERE `id` = ?', 1 ); + $source = $query->execute( array( $id ) )->fetchRow(); + + $fileOwner = false; + + if ( isset( $source['parent'] ) ) { + + $parent = $source['parent']; + + while ( isset( $parent ) ) { + + $query = \OC_DB::prepare( 'SELECT `parent`, `uid_owner` FROM `*PREFIX*share` WHERE `id` = ?', 1 ); + $item = $query->execute( array( $parent ) )->fetchRow(); + + if ( isset( $item['parent'] ) ) { + + $parent = $item['parent']; + + } else { + + $fileOwner = $item['uid_owner']; + + break; + + } + } + + } else { + + $fileOwner = $source['uid_owner']; + + } + + return $fileOwner; + + } + + /** + * @return string + */ + public function getUserId() { + return $this->userId; + } + + /** + * @return string + */ + public function getUserFilesDir() { + return $this->userFilesDir; + } + + /** + * @param $password + * @return bool + */ + public function checkRecoveryPassword( $password ) { + + $pathKey = '/owncloud_private_key/' . $this->recoveryKeyId . ".private.key"; + $pathControlData = '/control-file/controlfile.enc'; + + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + $recoveryKey = $this->view->file_get_contents( $pathKey ); + + $decryptedRecoveryKey = Crypt::symmetricDecryptFileContent( $recoveryKey, $password ); + + $controlData = $this->view->file_get_contents( $pathControlData ); + $decryptedControlData = Crypt::keyDecrypt( $controlData, $decryptedRecoveryKey ); + + \OC_FileProxy::$enabled = $proxyStatus; + + if ( $decryptedControlData === 'ownCloud' ) { + return true; + } + + return false; + } + + /** + * @return string + */ + public function getRecoveryKeyId() { + return $this->recoveryKeyId; + } + + /** + * @brief add recovery key to all encrypted files + */ + public function addRecoveryKeys( $path = '/' ) { + $dirContent = $this->view->getDirectoryContent( $this->keyfilesPath . $path ); + foreach ( $dirContent as $item ) { + // get relative path from files_encryption/keyfiles/ + $filePath = substr( $item['path'], strlen('files_encryption/keyfiles') ); + if ( $item['type'] == 'dir' ) { + $this->addRecoveryKeys( $filePath . '/' ); + } else { + $session = new Session( new \OC_FilesystemView( '/' ) ); + $sharingEnabled = \OCP\Share::isEnabled(); + $file = substr( $filePath, 0, -4 ); + $usersSharing = $this->getSharingUsersArray( $sharingEnabled, $file ); + $this->setSharedFileKeyfiles( $session, $usersSharing, $file ); + } + } + } + + /** + * @brief remove recovery key to all encrypted files + */ + public function removeRecoveryKeys( $path = '/' ) { + $dirContent = $this->view->getDirectoryContent( $this->keyfilesPath . $path ); + foreach ( $dirContent as $item ) { + // get relative path from files_encryption/keyfiles + $filePath = substr( $item['path'], strlen('files_encryption/keyfiles') ); + if ( $item['type'] == 'dir' ) { + $this->removeRecoveryKeys( $filePath . '/' ); + } else { + $file = substr( $filePath, 0, -4 ); + $this->view->unlink( $this->shareKeysPath . '/' . $file . '.' . $this->recoveryKeyId . '.shareKey' ); + } + } + } + + /** + * @brief decrypt given file with recovery key and encrypt it again to the owner and his new key + * @param string $file + * @param string $privateKey recovery key to decrypt the file + */ + private function recoverFile( $file, $privateKey ) { + + $sharingEnabled = \OCP\Share::isEnabled(); + + // Find out who, if anyone, is sharing the file + if ( $sharingEnabled ) { + $result = \OCP\Share::getUsersSharingFile( $file, $this->userId, true, true, true ); + $userIds = $result['users']; + $userIds[] = $this->recoveryKeyId; + if ( $result['public'] ) { + $userIds[] = $this->publicShareKeyId; + } + } else { + $userIds = array( $this->userId, $this->recoveryKeyId ); + } + $filteredUids = $this->filterShareReadyUsers( $userIds ); + + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + //decrypt file key + $encKeyfile = $this->view->file_get_contents( $this->keyfilesPath . $file . ".key" ); + $shareKey = $this->view->file_get_contents( $this->shareKeysPath . $file . "." . $this->recoveryKeyId . ".shareKey" ); + $plainKeyfile = Crypt::multiKeyDecrypt( $encKeyfile, $shareKey, $privateKey ); + // encrypt file key again to all users, this time with the new public key for the recovered use + $userPubKeys = Keymanager::getPublicKeys( $this->view, $filteredUids['ready'] ); + $multiEncKey = Crypt::multiKeyEncrypt( $plainKeyfile, $userPubKeys ); + + // write new keys to filesystem TDOO! + $this->view->file_put_contents( $this->keyfilesPath . $file . '.key', $multiEncKey['data'] ); + foreach ( $multiEncKey['keys'] as $userId => $shareKey ) { + $shareKeyPath = $this->shareKeysPath . $file . '.' . $userId . '.shareKey'; + $this->view->file_put_contents( $shareKeyPath, $shareKey ); + } + + // Return proxy to original status + \OC_FileProxy::$enabled = $proxyStatus; + } + + /** + * @brief collect all files and recover them one by one + * @param string $path to look for files keys + * @param string $privateKey private recovery key which is used to decrypt the files + */ + private function recoverAllFiles( $path, $privateKey ) { + $dirContent = $this->view->getDirectoryContent( $this->keyfilesPath . $path ); + foreach ( $dirContent as $item ) { + $filePath = substr( $item['path'], 25 ); + if ( $item['type'] == 'dir' ) { + $this->recoverAllFiles( $filePath . '/', $privateKey ); + } else { + $file = substr( $filePath, 0, -4 ); + $this->recoverFile( $file, $privateKey ); + } + } + } + + /** + * @brief recover users files in case of password lost + * @param string $recoveryPassword + */ + public function recoverUsersFiles( $recoveryPassword ) { + + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + $encryptedKey = $this->view->file_get_contents( '/owncloud_private_key/' . $this->recoveryKeyId . '.private.key' ); + $privateKey = Crypt::symmetricDecryptFileContent( $encryptedKey, $recoveryPassword ); + + \OC_FileProxy::$enabled = $proxyStatus; + + $this->recoverAllFiles( '/', $privateKey ); } } |