From 9d1e60325c6f478484ff8f70ff3cd13d9d7d4913 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Schie=C3=9Fle?= <schiessle@owncloud.com>
Date: Thu, 16 May 2013 14:53:04 +0200
Subject: [PATCH] allow admin to recover users files in case of password lost

---
 apps/files_encryption/hooks/hooks.php      | 73 ++++++++++++++-----
 apps/files_encryption/js/settings-admin.js |  2 +-
 apps/files_encryption/lib/helper.php       |  2 +-
 apps/files_encryption/lib/util.php         | 84 +++++++++++++++++++++-
 lib/user.php                               |  7 +-
 settings/ajax/changepassword.php           |  5 +-
 settings/css/settings.css                  |  2 +
 settings/js/users.js                       |  4 +-
 settings/templates/personal.php            |  2 +-
 settings/templates/users.php               |  5 ++
 settings/users.php                         |  3 +
 11 files changed, 159 insertions(+), 30 deletions(-)

diff --git a/apps/files_encryption/hooks/hooks.php b/apps/files_encryption/hooks/hooks.php
index f843c7027d..0af0845d7c 100644
--- a/apps/files_encryption/hooks/hooks.php
+++ b/apps/files_encryption/hooks/hooks.php
@@ -142,32 +142,67 @@ class Hooks {
 	 * @brief Change a user's encryption passphrase
 	 * @param array $params keys: uid, password
 	 */
-	public static function setPassphrase( $params ) {
-		
+	public static function setPassphrase($params) {
+
 		// Only attempt to change passphrase if server-side encryption
 		// is in use (client-side encryption does not have access to 
 		// the necessary keys)
-		if ( Crypt::mode() == 'server' ) {
+		if (Crypt::mode() == 'server') {
 
-            $view = new \OC_FilesystemView( '/' );
+			if ($params['uid'] == \OCP\User::getUser()) {
 
-			$session = new Session($view);
-			
-			// Get existing decrypted private key
-			$privateKey = $session->getPrivateKey();
-			
-			// Encrypt private key with new user pwd as passphrase
-			$encryptedPrivateKey = Crypt::symmetricEncryptFileContent( $privateKey, $params['password'] );
-			
-			// Save private key
-			Keymanager::setPrivateKey( $encryptedPrivateKey );
-			
-			// NOTE: Session does not need to be updated as the 
-			// private key has not changed, only the passphrase 
-			// used to decrypt it has changed
+				$view = new \OC_FilesystemView('/');
+
+				$session = new Session($view);
+
+				// Get existing decrypted private key
+				$privateKey = $session->getPrivateKey();
+
+				// Encrypt private key with new user pwd as passphrase
+				$encryptedPrivateKey = Crypt::symmetricEncryptFileContent($privateKey, $params['password']);
+
+				// Save private key
+				Keymanager::setPrivateKey($encryptedPrivateKey);
+
+				// NOTE: Session does not need to be updated as the
+				// private key has not changed, only the passphrase
+				// used to decrypt it has changed
 			
+				
+			} else { // admin changed the password for a different user, create new keys and reencrypt file keys
+				
+				$user = $params['uid'];
+				$recoveryPassword = $params['recoveryPassword'];
+				$newUserPassword = $params['password'];
+
+				$view = new \OC_FilesystemView('/');
+
+				// make sure that the users home is mounted
+				\OC\Files\Filesystem::initMountPoints($user);
+
+				$keypair = Crypt::createKeypair();
+				
+				// Disable encryption proxy to prevent recursive calls
+				$proxyStatus = \OC_FileProxy::$enabled;
+				\OC_FileProxy::$enabled = false;
+
+				// Save public key
+				$view->file_put_contents( '/public-keys/'.$user.'.public.key', $keypair['publicKey'] );
+
+				// Encrypt private key empthy passphrase
+				$encryptedPrivateKey = Crypt::symmetricEncryptFileContent( $keypair['privateKey'], $newUserPassword );
+
+				// Save private key
+				$view->file_put_contents( '/'.$user.'/files_encryption/'.$user.'.private.key', $encryptedPrivateKey );
+
+				if ( $recoveryPassword ) { // if recovery key is set we can re-encrypt the key files
+					$util = new Util($view, $user);
+					$util->recoverUsersFiles($recoveryPassword);
+				}
+
+				\OC_FileProxy::$enabled = $proxyStatus;
+			}
 		}
-	
 	}
 
 	/*
diff --git a/apps/files_encryption/js/settings-admin.js b/apps/files_encryption/js/settings-admin.js
index 9bc6ab6433..dbae42b011 100644
--- a/apps/files_encryption/js/settings-admin.js
+++ b/apps/files_encryption/js/settings-admin.js
@@ -69,7 +69,7 @@ $(document).ready(function(){
 		}
 	);
 
-	// change password
+	// change recovery password
 
 	$('input:password[name="changeRecoveryPassword"]').keyup(function(event) {
 		var oldRecoveryPassword = $('input:password[id="oldRecoveryPassword"]').val();
diff --git a/apps/files_encryption/lib/helper.php b/apps/files_encryption/lib/helper.php
index 6d5aae4e8b..86d860465e 100755
--- a/apps/files_encryption/lib/helper.php
+++ b/apps/files_encryption/lib/helper.php
@@ -46,7 +46,7 @@ class Helper {
     public static function registerUserHooks() {
 
         \OCP\Util::connectHook( 'OC_User', 'post_login', 'OCA\Encryption\Hooks', 'login' );
-        \OCP\Util::connectHook( 'OC_User', 'pre_setPassword', 'OCA\Encryption\Hooks', 'setPassphrase' );
+        \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' );
     }
diff --git a/apps/files_encryption/lib/util.php b/apps/files_encryption/lib/util.php
index 91d86cc855..fab807b014 100644
--- a/apps/files_encryption/lib/util.php
+++ b/apps/files_encryption/lib/util.php
@@ -929,7 +929,7 @@ class Util {
 
 		// Get the current users's private key for decrypting existing keyfile
 		$privateKey = $session->getPrivateKey();
-		
+
 		$fileOwner = \OC\Files\Filesystem::getOwner( $filePath );
 		
 		// Decrypt keyfile
@@ -1336,7 +1336,7 @@ class Util {
 		}
 	}
 
-		/**
+	/**
 	 * @brief remove recovery key to all encrypted files
 	 */
 	public function removeRecoveryKeys($path = '/') {
@@ -1351,4 +1351,84 @@ class Util {
 			}
 		}
 	}
+
+	/**
+	 * @brief decrypt given file with recovery key and encrypt it again to the owner and his new key
+	 * @param type $file
+	 * @param type $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 type $path to look for files keys
+	 * @param type $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->addRecoveryKey($filePath . '/', $privateKey);
+			} else {
+				$file = substr($filePath, 0, -4);
+				$this->recoverFile($file, $privateKey);
+			}
+		}
+	}
+
+	/**
+	 * @brief recover users files in case of password lost
+	 * @param type $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);
+	}
 }
diff --git a/lib/user.php b/lib/user.php
index 226b716188..833e886659 100644
--- a/lib/user.php
+++ b/lib/user.php
@@ -393,13 +393,14 @@ class OC_User {
 	 * @brief Set password
 	 * @param $uid The username
 	 * @param $password The new password
+	 * @param $recoveryPassword for the encryption app to reset encryption keys
 	 * @returns true/false
 	 *
 	 * Change the password of a user
 	 */
-	public static function setPassword( $uid, $password ) {
+	public static function setPassword( $uid, $password, $recoveryPassword = null ) {
 		$run = true;
-		OC_Hook::emit( "OC_User", "pre_setPassword", array( "run" => &$run, "uid" => $uid, "password" => $password ));
+		OC_Hook::emit( "OC_User", "pre_setPassword", array( "run" => &$run, "uid" => $uid, "password" => $password, "recoveryPassword" => $recoveryPassword ));
 
 		if( $run ) {
 			$success = false;
@@ -412,7 +413,7 @@ class OC_User {
 			}
 			// invalidate all login cookies
 			OC_Preferences::deleteApp($uid, 'login_token');
-			OC_Hook::emit( "OC_User", "post_setPassword", array( "uid" => $uid, "password" => $password ));
+			OC_Hook::emit( "OC_User", "post_setPassword", array( "uid" => $uid, "password" => $password, "recoveryPassword" => $recoveryPassword ));
 			return $success;
 		}
 		else{
diff --git a/settings/ajax/changepassword.php b/settings/ajax/changepassword.php
index 4f16bff63d..fe63f27a6e 100644
--- a/settings/ajax/changepassword.php
+++ b/settings/ajax/changepassword.php
@@ -8,8 +8,9 @@ OC_JSON::checkLoggedIn();
 OC_APP::loadApps();
 
 $username = isset($_POST["username"]) ? $_POST["username"] : OC_User::getUser();
-$password = isset($_POST["newpassword"]) ? $_POST["newpassword"] : null;
+$password = isset($_POST["password"]) ? $_POST["password"] : null;
 $oldPassword=isset($_POST["oldpassword"])?$_POST["oldpassword"]:'';
+$recoveryPassword=isset($_POST["recoveryPassword"])?$_POST["recoveryPassword"]:null;
 
 $userstatus = null;
 if(OC_User::isAdminUser(OC_User::getUser())) {
@@ -28,7 +29,7 @@ if(is_null($userstatus)) {
 }
 
 // Return Success story
-if(!is_null($password) && OC_User::setPassword( $username, $password )) {
+if(!is_null($password) && OC_User::setPassword( $username, $password, $recoveryPassword )) {
 	OC_JSON::success(array("data" => array( "username" => $username )));
 }
 else{
diff --git a/settings/css/settings.css b/settings/css/settings.css
index 46a0bbe7c3..950e892901 100644
--- a/settings/css/settings.css
+++ b/settings/css/settings.css
@@ -45,6 +45,8 @@ table:not(.nostyle) { width:100%; }
 #rightcontent { padding-left: 1em; }
 div.quota { float:right; display:block; position:absolute; right:25em; top:-1px; }
 div.quota-select-wrapper { position: relative; }
+div.recoveryPassword { left:50em; display:block; position:absolute; top:-1px; }
+input#recoveryPassword {width:15em;}
 select.quota { position:absolute; left:0; top:0; width:10em; }
 select.quota-user { position:relative; left:0; top:0; width:10em; }
 div.quota>span { position:absolute; right:0; white-space:nowrap; top:.7em; color:#888; text-shadow:0 1px 0 #fff; }
diff --git a/settings/js/users.js b/settings/js/users.js
index 690c9ad046..9bd7f31f0b 100644
--- a/settings/js/users.js
+++ b/settings/js/users.js
@@ -351,9 +351,11 @@ $(document).ready(function () {
 		input.keypress(function (event) {
 			if (event.keyCode == 13) {
 				if ($(this).val().length > 0) {
+		var recoveryPasswordVal = $('input:password[id="recoveryPassword"]').val();
+		console.log("RECOVERY PASSWD: " + recoveryPasswordVal);
 					$.post(
 						OC.filePath('settings', 'ajax', 'changepassword.php'),
-						{username: uid, password: $(this).val()},
+						{username: uid, password: $(this).val(), recoveryPassword: recoveryPasswordVal},
 						function (result) {
 						}
 					);
diff --git a/settings/templates/personal.php b/settings/templates/personal.php
index cfb45e99c4..da812e8ed9 100644
--- a/settings/templates/personal.php
+++ b/settings/templates/personal.php
@@ -38,7 +38,7 @@ if($_['passwordChangeSupported']) {
 		<div id="passwordchanged"><?php echo $l->t('Your password was changed');?></div>
 		<div id="passworderror"><?php echo $l->t('Unable to change your password');?></div>
 		<input type="password" id="pass1" name="oldpassword" placeholder="<?php echo $l->t('Current password');?>" />
-		<input type="password" id="pass2" name="newpassword"
+		<input type="password" id="pass2" name="password"
 			placeholder="<?php echo $l->t('New password');?>" data-typetoggle="#personal-show" />
 		<input type="checkbox" id="personal-show" name="show" /><label for="personal-show"></label>
 		<input id="passwordbutton" type="submit" value="<?php echo $l->t('Change password');?>" />
diff --git a/settings/templates/users.php b/settings/templates/users.php
index e86dd46efb..a6df85983d 100644
--- a/settings/templates/users.php
+++ b/settings/templates/users.php
@@ -29,6 +29,11 @@ $_['subadmingroups'] = array_flip($items);
 			<?php endforeach;?>
 		</select> <input type="submit" value="<?php p($l->t('Create'))?>" />
 	</form>
+	<?php if((bool)$_['recoveryAdminEnabled']): ?>
+	<div class="recoveryPassword">
+	<input id="recoveryPassword" type="password" placeholder="<?php p($l->t('Admin Recovery Password'))?>" />
+	</div>
+	<?php endif; ?>
 	<div class="quota">
 		<span><?php p($l->t('Default Storage'));?></span>
 			<?php if((bool) $_['isadmin']): ?>
diff --git a/settings/users.php b/settings/users.php
index 94e6d0a9a1..e5c8a7aaa8 100644
--- a/settings/users.php
+++ b/settings/users.php
@@ -20,6 +20,8 @@ $users = array();
 $groups = array();
 
 $isadmin = OC_User::isAdminUser(OC_User::getUser());
+$recoveryAdminEnabled = OC_App::isEnabled('files_encryption') &&
+					    OC_Appconfig::getValue( 'files_encryption', 'recoveryAdminEnabled' );
 
 if($isadmin) {
 	$accessiblegroups = OC_Group::getGroups();
@@ -77,4 +79,5 @@ $tmpl->assign( 'numofgroups', count($accessiblegroups));
 $tmpl->assign( 'quota_preset', $quotaPreset);
 $tmpl->assign( 'default_quota', $defaultQuota);
 $tmpl->assign( 'defaultQuotaIsUserDefined', $defaultQuotaIsUserDefined);
+$tmpl->assign( 'recoveryAdminEnabled', $recoveryAdminEnabled);
 $tmpl->printPage();
-- 
GitLab