1638 lines
45 KiB
PHP
1638 lines
45 KiB
PHP
<?php
|
|
/**
|
|
* ownCloud
|
|
*
|
|
* @copyright (C) 2014 ownCloud, Inc.
|
|
*
|
|
* @author Sam Tuke <samtuke@owncloud.com>,
|
|
* @author Frank Karlitschek <frank@owncloud.org>,
|
|
* @author Bjoern Schiessle <schiessle@owncloud.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/>.
|
|
*
|
|
*/
|
|
|
|
namespace OCA\Files_Encryption;
|
|
|
|
/**
|
|
* Class for utilities relating to encrypted file storage system
|
|
* @param \OC\Files\View $view expected to have OC '/' as root path
|
|
* @param string $userId ID of the logged in user
|
|
* @param int $client indicating status of client side encryption. Currently
|
|
* unused, likely to become obsolete shortly
|
|
*/
|
|
|
|
class Util {
|
|
|
|
const MIGRATION_COMPLETED = 1; // migration to new encryption completed
|
|
const MIGRATION_IN_PROGRESS = -1; // migration is running
|
|
const MIGRATION_OPEN = 0; // user still needs to be migrated
|
|
|
|
const FILE_TYPE_FILE = 0;
|
|
const FILE_TYPE_VERSION = 1;
|
|
const FILE_TYPE_CACHE = 2;
|
|
|
|
private $view; // OC\Files\View object for filesystem operations
|
|
private $userId; // ID of the user we use to encrypt/decrypt files
|
|
private $keyId; // ID of the key we want to manipulate
|
|
private $client; // Client side encryption mode flag
|
|
private $publicKeyDir; // Dir containing all public user keys
|
|
private $encryptionDir; // Dir containing user's files_encryption
|
|
private $keysPath; // Dir containing all file related encryption keys
|
|
private $publicKeyPath; // Path to user's public key
|
|
private $privateKeyPath; // Path to user's private key
|
|
private $userFilesDir;
|
|
private $publicShareKeyId;
|
|
private $recoveryKeyId;
|
|
private $isPublic;
|
|
|
|
/**
|
|
* @param \OC\Files\View $view
|
|
* @param string $userId
|
|
* @param bool $client
|
|
*/
|
|
public function __construct($view, $userId, $client = false) {
|
|
|
|
$this->view = $view;
|
|
$this->client = $client;
|
|
$this->userId = $userId;
|
|
|
|
$appConfig = \OC::$server->getAppConfig();
|
|
|
|
$this->publicShareKeyId = $appConfig->getValue('files_encryption', 'publicShareKeyId');
|
|
$this->recoveryKeyId = $appConfig->getValue('files_encryption', 'recoveryKeyId');
|
|
|
|
$this->userDir = '/' . $this->userId;
|
|
$this->fileFolderName = 'files';
|
|
$this->userFilesDir =
|
|
'/' . $userId . '/' . $this->fileFolderName; // TODO: Does this need to be user configurable?
|
|
$this->publicKeyDir = Keymanager::getPublicKeyPath();
|
|
$this->encryptionDir = '/' . $this->userId . '/' . 'files_encryption';
|
|
$this->keysPath = $this->encryptionDir . '/' . 'keys';
|
|
$this->publicKeyPath =
|
|
$this->publicKeyDir . '/' . $this->userId . '.publicKey'; // e.g. data/public-keys/admin.publicKey
|
|
$this->privateKeyPath =
|
|
$this->encryptionDir . '/' . $this->userId . '.privateKey'; // e.g. data/admin/admin.privateKey
|
|
// make sure that the owners home is mounted
|
|
\OC\Files\Filesystem::initMountPoints($userId);
|
|
|
|
if (Helper::isPublicAccess()) {
|
|
$this->keyId = $this->publicShareKeyId;
|
|
$this->isPublic = true;
|
|
} else {
|
|
$this->keyId = $this->userId;
|
|
$this->isPublic = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public function ready() {
|
|
|
|
if (
|
|
!$this->view->file_exists($this->encryptionDir)
|
|
or !$this->view->file_exists($this->keysPath)
|
|
or !$this->view->file_exists($this->publicKeyPath)
|
|
or !$this->view->file_exists($this->privateKeyPath)
|
|
) {
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* check if the users private & public key exists
|
|
* @return boolean
|
|
*/
|
|
public function userKeysExists() {
|
|
if (
|
|
$this->view->file_exists($this->privateKeyPath) &&
|
|
$this->view->file_exists($this->publicKeyPath)) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* create a new public/private key pair for the user
|
|
*
|
|
* @param string $password password for the private key
|
|
*/
|
|
public function replaceUserKeys($password) {
|
|
$this->backupAllKeys('password_reset');
|
|
$this->view->unlink($this->publicKeyPath);
|
|
$this->view->unlink($this->privateKeyPath);
|
|
$this->setupServerSide($password);
|
|
}
|
|
|
|
/**
|
|
* Sets up user folders and keys for serverside encryption
|
|
*
|
|
* @param string $passphrase to encrypt server-stored private key with
|
|
* @return bool
|
|
*/
|
|
public function setupServerSide($passphrase = null) {
|
|
|
|
// Set directories to check / create
|
|
$setUpDirs = array(
|
|
$this->userDir,
|
|
$this->publicKeyDir,
|
|
$this->encryptionDir,
|
|
$this->keysPath
|
|
);
|
|
|
|
// Check / create all necessary dirs
|
|
foreach ($setUpDirs as $dirPath) {
|
|
|
|
if (!$this->view->file_exists($dirPath)) {
|
|
|
|
$this->view->mkdir($dirPath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Create user keypair
|
|
// we should never override a keyfile
|
|
if (
|
|
!$this->view->file_exists($this->publicKeyPath)
|
|
&& !$this->view->file_exists($this->privateKeyPath)
|
|
) {
|
|
|
|
// Generate keypair
|
|
$keypair = Crypt::createKeypair();
|
|
|
|
if ($keypair) {
|
|
|
|
\OC_FileProxy::$enabled = false;
|
|
|
|
// Encrypt private key with user pwd as passphrase
|
|
$encryptedPrivateKey = Crypt::symmetricEncryptFileContent($keypair['privateKey'], $passphrase, Helper::getCipher());
|
|
|
|
// Save key-pair
|
|
if ($encryptedPrivateKey) {
|
|
$header = crypt::generateHeader();
|
|
$this->view->file_put_contents($this->privateKeyPath, $header . $encryptedPrivateKey);
|
|
$this->view->file_put_contents($this->publicKeyPath, $keypair['publicKey']);
|
|
}
|
|
|
|
\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)) {
|
|
\OCP\Util::writeLog('Encryption library',
|
|
'public key exists but private key is missing for "' . $this->keyId . '"', \OCP\Util::FATAL);
|
|
return false;
|
|
} else {
|
|
if (!$this->view->file_exists($this->publicKeyPath) && $this->view->file_exists($this->privateKeyPath)
|
|
) {
|
|
\OCP\Util::writeLog('Encryption library',
|
|
'private key exists but public key is missing for "' . $this->keyId . '"', \OCP\Util::FATAL);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getPublicShareKeyId() {
|
|
return $this->publicShareKeyId;
|
|
}
|
|
|
|
/**
|
|
* 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() {
|
|
|
|
$recoveryMode = \OC::$server->getConfig()->getUserValue($this->userId, 'files_encryption', 'recovery_enabled', '0');
|
|
|
|
return ($recoveryMode === '1') ? true : false;
|
|
|
|
}
|
|
|
|
/**
|
|
* Enable / disable pwd recovery for a given user
|
|
* @param bool $enabled Whether to enable or disable recovery
|
|
* @return bool
|
|
*/
|
|
public function setRecoveryForUser($enabled) {
|
|
|
|
$value = $enabled ? '1' : '0';
|
|
try {
|
|
\OC::$server->getConfig()->setUserValue($this->userId, 'files_encryption', 'recovery_enabled', $value);
|
|
return true;
|
|
} catch(\OCP\PreConditionNotMetException $e) {
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Find all files and their encryption status within a directory
|
|
* @param string $directory The path of the parent directory to search
|
|
* @param bool $found the founded files if called again
|
|
* @return array keys: plain, encrypted, broken
|
|
* @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 findEncFiles($directory, &$found = false) {
|
|
|
|
// Disable proxy - we don't want files to be decrypted before
|
|
// we handle them
|
|
\OC_FileProxy::$enabled = false;
|
|
|
|
if ($found === false) {
|
|
$found = array(
|
|
'plain' => array(),
|
|
'encrypted' => array(),
|
|
'broken' => array(),
|
|
);
|
|
}
|
|
|
|
if ($this->view->is_dir($directory) && $handle = $this->view->opendir($directory)){
|
|
if (is_resource($handle)) {
|
|
while (false !== ($file = readdir($handle))) {
|
|
|
|
if ($file !== "." && $file !== "..") {
|
|
|
|
$filePath = $directory . '/' . $this->view->getRelativePath('/' . $file);
|
|
$relPath = Helper::stripUserFilesPath($filePath);
|
|
|
|
// If the path is a directory, search
|
|
// its contents
|
|
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;
|
|
|
|
$isEncryptedPath = $this->isEncryptedPath($filePath);
|
|
// If the file is encrypted
|
|
// NOTE: If the userId is
|
|
// empty or not set, file will
|
|
// detected as plain
|
|
// NOTE: This is inefficient;
|
|
// scanning every file like this
|
|
// will eat server resources :(
|
|
if ($isEncryptedPath) {
|
|
|
|
$fileKey = Keymanager::getFileKey($this->view, $this, $relPath);
|
|
$shareKey = Keymanager::getShareKey($this->view, $this->userId, $this, $relPath);
|
|
// if file is encrypted but now file key is available, throw exception
|
|
if ($fileKey === false || $shareKey === false) {
|
|
\OCP\Util::writeLog('encryption library', 'No keys available to decrypt the file: ' . $filePath, \OCP\Util::ERROR);
|
|
$found['broken'][] = array(
|
|
'name' => $file,
|
|
'path' => $filePath,
|
|
);
|
|
} else {
|
|
$found['encrypted'][] = array(
|
|
'name' => $file,
|
|
'path' => $filePath,
|
|
);
|
|
}
|
|
|
|
// If the file is not encrypted
|
|
} else {
|
|
|
|
$found['plain'][] = array(
|
|
'name' => $file,
|
|
'path' => $relPath
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
\OC_FileProxy::$enabled = true;
|
|
|
|
return $found;
|
|
}
|
|
|
|
/**
|
|
* Check if a given path identifies an encrypted file
|
|
* @param string $path
|
|
* @return boolean
|
|
*/
|
|
public function isEncryptedPath($path) {
|
|
|
|
// Disable encryption proxy so data retrieved is in its
|
|
// original form
|
|
$proxyStatus = \OC_FileProxy::$enabled;
|
|
\OC_FileProxy::$enabled = false;
|
|
|
|
$data = '';
|
|
|
|
// we only need 24 byte from the last chunk
|
|
if ($this->view->file_exists($path)) {
|
|
$handle = $this->view->fopen($path, 'r');
|
|
if (is_resource($handle)) {
|
|
// suppress fseek warining, we handle the case that fseek doesn't
|
|
// work in the else branch
|
|
if (@fseek($handle, -24, SEEK_END) === 0) {
|
|
$data = fgets($handle);
|
|
} else {
|
|
// if fseek failed on the storage we create a local copy from the file
|
|
// and read this one
|
|
fclose($handle);
|
|
$localFile = $this->view->getLocalFile($path);
|
|
$handle = fopen($localFile, 'r');
|
|
if (is_resource($handle) && fseek($handle, -24, SEEK_END) === 0) {
|
|
$data = fgets($handle);
|
|
}
|
|
}
|
|
fclose($handle);
|
|
}
|
|
}
|
|
|
|
// re-enable proxy
|
|
\OC_FileProxy::$enabled = $proxyStatus;
|
|
|
|
return Crypt::isCatfileContent($data);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
// split the path parts
|
|
$pathParts = explode('/', $path);
|
|
|
|
if (isset($pathParts[2]) && $pathParts[2] === 'files' && $this->view->file_exists($path)
|
|
&& $this->isEncryptedPath($path)
|
|
) {
|
|
|
|
$cipher = 'AES-128-CFB';
|
|
$realSize = 0;
|
|
|
|
// get the size from filesystem
|
|
$size = $this->view->filesize($path);
|
|
|
|
// open stream
|
|
$stream = $this->view->fopen($path, "r");
|
|
|
|
if (is_resource($stream)) {
|
|
|
|
// if the file contains a encryption header we
|
|
// we set the cipher
|
|
// and we update the size
|
|
if ($this->containHeader($path)) {
|
|
$data = fread($stream,Crypt::BLOCKSIZE);
|
|
$header = Crypt::parseHeader($data);
|
|
$cipher = Crypt::getCipher($header);
|
|
$size -= Crypt::BLOCKSIZE;
|
|
}
|
|
|
|
// fast path, else the calculation for $lastChunkNr is bogus
|
|
if ($size === 0) {
|
|
\OC_FileProxy::$enabled = $proxyStatus;
|
|
return 0;
|
|
}
|
|
|
|
// calculate last chunk nr
|
|
// next highest is end of chunks, one subtracted is last one
|
|
// we have to read the last chunk, we can't just calculate it (because of padding etc)
|
|
$lastChunkNr = ceil($size/Crypt::BLOCKSIZE)-1;
|
|
|
|
// calculate last chunk position
|
|
$lastChunkPos = ($lastChunkNr * Crypt::BLOCKSIZE);
|
|
|
|
// get the content of the last chunk
|
|
if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
|
|
$realSize+=$lastChunkNr*6126;
|
|
}
|
|
$lastChunkContentEncrypted='';
|
|
$count=Crypt::BLOCKSIZE;
|
|
while ($count>0) {
|
|
$data=fread($stream,Crypt::BLOCKSIZE);
|
|
$count=strlen($data);
|
|
$lastChunkContentEncrypted.=$data;
|
|
if(strlen($lastChunkContentEncrypted)>Crypt::BLOCKSIZE) {
|
|
$realSize+=6126;
|
|
$lastChunkContentEncrypted=substr($lastChunkContentEncrypted,Crypt::BLOCKSIZE);
|
|
}
|
|
}
|
|
fclose($stream);
|
|
$relPath = Helper::stripUserFilesPath($path);
|
|
$shareKey = Keymanager::getShareKey($this->view, $this->keyId, $this, $relPath);
|
|
if($shareKey===false) {
|
|
\OC_FileProxy::$enabled = $proxyStatus;
|
|
return $result;
|
|
}
|
|
$session = new Session($this->view);
|
|
$privateKey = $session->getPrivateKey();
|
|
$plainKeyfile = $this->decryptKeyfile($relPath, $privateKey);
|
|
$plainKey = Crypt::multiKeyDecrypt($plainKeyfile, $shareKey, $privateKey);
|
|
$lastChunkContent=Crypt::symmetricDecryptFileContent($lastChunkContentEncrypted, $plainKey, $cipher);
|
|
|
|
// calc the real file size with the size of the last chunk
|
|
$realSize += strlen($lastChunkContent);
|
|
|
|
// store file size
|
|
$result = $realSize;
|
|
}
|
|
}
|
|
|
|
\OC_FileProxy::$enabled = $proxyStatus;
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* check if encrypted file contain a encryption header
|
|
*
|
|
* @param string $path
|
|
* @return boolean
|
|
*/
|
|
private function containHeader($path) {
|
|
// Disable encryption proxy to read the raw data
|
|
$proxyStatus = \OC_FileProxy::$enabled;
|
|
\OC_FileProxy::$enabled = false;
|
|
|
|
$isHeader = false;
|
|
$handle = $this->view->fopen($path, 'r');
|
|
|
|
if (is_resource($handle)) {
|
|
$firstBlock = fread($handle, Crypt::BLOCKSIZE);
|
|
$isHeader = Crypt::isHeader($firstBlock);
|
|
}
|
|
|
|
\OC_FileProxy::$enabled = $proxyStatus;
|
|
|
|
return $isHeader;
|
|
}
|
|
|
|
/**
|
|
* fix the file size of the encrypted file
|
|
* @param string $path absolute path
|
|
* @return boolean 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;
|
|
}
|
|
|
|
/**
|
|
* encrypt versions from given file
|
|
* @param array $filelist list of encrypted files, relative to data/user/files
|
|
* @return boolean
|
|
*/
|
|
private function encryptVersions($filelist) {
|
|
|
|
$successful = true;
|
|
|
|
if (\OCP\App::isEnabled('files_versions')) {
|
|
|
|
foreach ($filelist as $filename) {
|
|
|
|
$versions = \OCA\Files_Versions\Storage::getVersions($this->userId, $filename);
|
|
foreach ($versions as $version) {
|
|
|
|
$path = '/' . $this->userId . '/files_versions/' . $version['path'] . '.v' . $version['version'];
|
|
|
|
$encHandle = fopen('crypt://' . $path . '.part', 'wb');
|
|
|
|
if ($encHandle === false) {
|
|
\OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $path . '", decryption failed!', \OCP\Util::FATAL);
|
|
$successful = false;
|
|
continue;
|
|
}
|
|
|
|
$plainHandle = $this->view->fopen($path, 'rb');
|
|
if ($plainHandle === false) {
|
|
\OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $path . '.part", decryption failed!', \OCP\Util::FATAL);
|
|
$successful = false;
|
|
continue;
|
|
}
|
|
|
|
stream_copy_to_stream($plainHandle, $encHandle);
|
|
|
|
fclose($encHandle);
|
|
fclose($plainHandle);
|
|
|
|
$this->view->rename($path . '.part', $path);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $successful;
|
|
}
|
|
|
|
/**
|
|
* decrypt versions from given file
|
|
* @param string $filelist list of decrypted files, relative to data/user/files
|
|
* @return boolean
|
|
*/
|
|
private function decryptVersions($filelist) {
|
|
|
|
$successful = true;
|
|
|
|
if (\OCP\App::isEnabled('files_versions')) {
|
|
|
|
foreach ($filelist as $filename) {
|
|
|
|
$versions = \OCA\Files_Versions\Storage::getVersions($this->userId, $filename);
|
|
foreach ($versions as $version) {
|
|
|
|
$path = '/' . $this->userId . '/files_versions/' . $version['path'] . '.v' . $version['version'];
|
|
|
|
$encHandle = fopen('crypt://' . $path, 'rb');
|
|
|
|
if ($encHandle === false) {
|
|
\OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $path . '", decryption failed!', \OCP\Util::FATAL);
|
|
$successful = false;
|
|
continue;
|
|
}
|
|
|
|
$plainHandle = $this->view->fopen($path . '.part', 'wb');
|
|
if ($plainHandle === false) {
|
|
\OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $path . '.part", decryption failed!', \OCP\Util::FATAL);
|
|
$successful = false;
|
|
continue;
|
|
}
|
|
|
|
stream_copy_to_stream($encHandle, $plainHandle);
|
|
|
|
fclose($encHandle);
|
|
fclose($plainHandle);
|
|
|
|
$this->view->rename($path . '.part', $path);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $successful;
|
|
}
|
|
|
|
/**
|
|
* Decrypt all files
|
|
* @return bool
|
|
*/
|
|
public function decryptAll() {
|
|
|
|
$found = $this->findEncFiles($this->userId . '/files');
|
|
|
|
$successful = true;
|
|
|
|
if ($found) {
|
|
|
|
$versionStatus = \OCP\App::isEnabled('files_versions');
|
|
\OC_App::disable('files_versions');
|
|
|
|
$decryptedFiles = array();
|
|
|
|
// Encrypt unencrypted files
|
|
foreach ($found['encrypted'] as $encryptedFile) {
|
|
|
|
//relative to data/<user>/file
|
|
$relPath = Helper::stripUserFilesPath($encryptedFile['path']);
|
|
|
|
//get file info
|
|
$fileInfo = \OC\Files\Filesystem::getFileInfo($relPath);
|
|
|
|
//relative to /data
|
|
$rawPath = $encryptedFile['path'];
|
|
|
|
//get timestamp
|
|
$timestamp = $fileInfo['mtime'];
|
|
|
|
//enable proxy to use OC\Files\View to access the original file
|
|
\OC_FileProxy::$enabled = true;
|
|
|
|
// Open enc file handle for binary reading
|
|
$encHandle = $this->view->fopen($rawPath, 'rb');
|
|
|
|
// Disable proxy to prevent file being encrypted again
|
|
\OC_FileProxy::$enabled = false;
|
|
|
|
if ($encHandle === false) {
|
|
\OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $rawPath . '", decryption failed!', \OCP\Util::FATAL);
|
|
$successful = false;
|
|
continue;
|
|
}
|
|
|
|
// Open plain file handle for binary writing, with same filename as original plain file
|
|
$plainHandle = $this->view->fopen($rawPath . '.part', 'wb');
|
|
if ($plainHandle === false) {
|
|
\OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $rawPath . '.part", decryption failed!', \OCP\Util::FATAL);
|
|
$successful = false;
|
|
continue;
|
|
}
|
|
|
|
// Move plain file to a temporary location
|
|
$size = stream_copy_to_stream($encHandle, $plainHandle);
|
|
if ($size === 0) {
|
|
\OCP\Util::writeLog('Encryption library', 'Zero bytes copied of "' . $rawPath . '", decryption failed!', \OCP\Util::FATAL);
|
|
$successful = false;
|
|
continue;
|
|
}
|
|
|
|
fclose($encHandle);
|
|
fclose($plainHandle);
|
|
|
|
$fakeRoot = $this->view->getRoot();
|
|
$this->view->chroot('/' . $this->userId . '/files');
|
|
|
|
$this->view->rename($relPath . '.part', $relPath);
|
|
|
|
//set timestamp
|
|
$this->view->touch($relPath, $timestamp);
|
|
|
|
$this->view->chroot($fakeRoot);
|
|
|
|
// Add the file to the cache
|
|
\OC\Files\Filesystem::putFileInfo($relPath, array(
|
|
'encrypted' => false,
|
|
'size' => $size,
|
|
'unencrypted_size' => 0,
|
|
'etag' => $fileInfo['etag']
|
|
));
|
|
|
|
$decryptedFiles[] = $relPath;
|
|
|
|
}
|
|
|
|
if ($versionStatus) {
|
|
\OC_App::enable('files_versions');
|
|
}
|
|
|
|
if (!$this->decryptVersions($decryptedFiles)) {
|
|
$successful = false;
|
|
}
|
|
|
|
// if there are broken encrypted files than the complete decryption
|
|
// was not successful
|
|
if (!empty($found['broken'])) {
|
|
$successful = false;
|
|
}
|
|
|
|
if ($successful) {
|
|
$this->backupAllKeys('decryptAll', false, false);
|
|
$this->view->deleteAll($this->keysPath);
|
|
}
|
|
|
|
\OC_FileProxy::$enabled = true;
|
|
}
|
|
|
|
return $successful;
|
|
}
|
|
|
|
/**
|
|
* Encrypt all files in a directory
|
|
* @param string $dirPath the directory whose files will be encrypted
|
|
* @return bool
|
|
* @note Encryption is recursive
|
|
*/
|
|
public function encryptAll($dirPath) {
|
|
|
|
$result = true;
|
|
|
|
$found = $this->findEncFiles($dirPath);
|
|
|
|
// Disable proxy to prevent file being encrypted twice
|
|
\OC_FileProxy::$enabled = false;
|
|
|
|
$versionStatus = \OCP\App::isEnabled('files_versions');
|
|
\OC_App::disable('files_versions');
|
|
|
|
$encryptedFiles = array();
|
|
|
|
// Encrypt unencrypted files
|
|
foreach ($found['plain'] as $plainFile) {
|
|
|
|
//get file info
|
|
$fileInfo = \OC\Files\Filesystem::getFileInfo($plainFile['path']);
|
|
|
|
//relative to data/<user>/file
|
|
$relPath = $plainFile['path'];
|
|
|
|
//relative to /data
|
|
$rawPath = '/' . $this->userId . '/files/' . $plainFile['path'];
|
|
|
|
// keep timestamp
|
|
$timestamp = $fileInfo['mtime'];
|
|
|
|
// Open plain file handle for binary reading
|
|
$plainHandle = $this->view->fopen($rawPath, 'rb');
|
|
|
|
// Open enc file handle for binary writing, with same filename as original plain file
|
|
$encHandle = fopen('crypt://' . $rawPath . '.part', 'wb');
|
|
|
|
if (is_resource($encHandle) && is_resource($plainHandle)) {
|
|
// Move plain file to a temporary location
|
|
$size = stream_copy_to_stream($plainHandle, $encHandle);
|
|
|
|
fclose($encHandle);
|
|
fclose($plainHandle);
|
|
|
|
$fakeRoot = $this->view->getRoot();
|
|
$this->view->chroot('/' . $this->userId . '/files');
|
|
|
|
$this->view->rename($relPath . '.part', $relPath);
|
|
|
|
// set timestamp
|
|
$this->view->touch($relPath, $timestamp);
|
|
|
|
$encSize = $this->view->filesize($relPath);
|
|
|
|
$this->view->chroot($fakeRoot);
|
|
|
|
// Add the file to the cache
|
|
\OC\Files\Filesystem::putFileInfo($relPath, array(
|
|
'encrypted' => true,
|
|
'size' => $encSize,
|
|
'unencrypted_size' => $size,
|
|
'etag' => $fileInfo['etag']
|
|
));
|
|
|
|
$encryptedFiles[] = $relPath;
|
|
} else {
|
|
\OCP\Util::writeLog('files_encryption', 'initial encryption: could not encrypt ' . $rawPath, \OCP\Util::FATAL);
|
|
$result = false;
|
|
}
|
|
}
|
|
|
|
\OC_FileProxy::$enabled = true;
|
|
|
|
if ($versionStatus) {
|
|
\OC_App::enable('files_versions');
|
|
}
|
|
|
|
$result = $result && $this->encryptVersions($encryptedFiles);
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
/**
|
|
* 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 'keysPath':
|
|
|
|
return $this->keysPath;
|
|
|
|
break;
|
|
|
|
case 'publicKeyPath':
|
|
|
|
return $this->publicKeyPath;
|
|
|
|
break;
|
|
|
|
case 'privateKeyPath':
|
|
|
|
return $this->privateKeyPath;
|
|
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns whether the given user is ready for encryption.
|
|
* Also returns true if the given user is the public user
|
|
* or the recovery key user.
|
|
*
|
|
* @param string $user user to check
|
|
*
|
|
* @return boolean true if the user is ready, false otherwise
|
|
*/
|
|
private function isUserReady($user) {
|
|
if ($user === $this->publicShareKeyId
|
|
|| $user === $this->recoveryKeyId
|
|
) {
|
|
return true;
|
|
}
|
|
$util = new Util($this->view, $user);
|
|
return $util->ready();
|
|
}
|
|
|
|
/**
|
|
* Filter an array of UIDs to return only ones ready for sharing
|
|
* @param array $unfilteredUsers users to be checked for sharing readiness
|
|
* @return array as 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) {
|
|
// Check that the user is encryption capable, or is the
|
|
// public system user (for public shares)
|
|
if ($this->isUserReady($user)) {
|
|
|
|
// 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
|
|
\OCP\Util::writeLog('Encryption library',
|
|
'"' . $user . '" is not setup for encryption', \OCP\Util::WARN);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return array(
|
|
'ready' => $readyIds,
|
|
'unready' => $unreadyIds
|
|
);
|
|
|
|
}
|
|
|
|
/**
|
|
* Decrypt a keyfile
|
|
* @param string $filePath
|
|
* @param string $privateKey
|
|
* @return false|string
|
|
*/
|
|
private function decryptKeyfile($filePath, $privateKey) {
|
|
|
|
// Get the encrypted keyfile
|
|
$encKeyfile = Keymanager::getFileKey($this->view, $this, $filePath);
|
|
|
|
// The file has a shareKey and must use it for decryption
|
|
$shareKey = Keymanager::getShareKey($this->view, $this->keyId, $this, $filePath);
|
|
|
|
$plainKeyfile = Crypt::multiKeyDecrypt($encKeyfile, $shareKey, $privateKey);
|
|
|
|
return $plainKeyfile;
|
|
}
|
|
|
|
/**
|
|
* 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'])) {
|
|
|
|
\OCP\Util::writeLog('Encryption library',
|
|
'Sharing to these user(s) failed as they are unready for encryption:"'
|
|
. print_r($filteredUids['unready'], 1), \OCP\Util::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();
|
|
|
|
try {
|
|
// Decrypt keyfile
|
|
$plainKeyfile = $this->decryptKeyfile($filePath, $privateKey);
|
|
// Re-enc keyfile to (additional) sharekeys
|
|
$multiEncKey = Crypt::multiKeyEncrypt($plainKeyfile, $userPubKeys);
|
|
} catch (Exception\EncryptionException $e) {
|
|
$msg = 'set shareFileKeyFailed (code: ' . $e->getCode() . '): ' . $e->getMessage();
|
|
\OCP\Util::writeLog('files_encryption', $msg, \OCP\Util::FATAL);
|
|
return false;
|
|
} catch (\Exception $e) {
|
|
$msg = 'set shareFileKeyFailed (unknown error): ' . $e->getMessage();
|
|
\OCP\Util::writeLog('files_encryption', $msg, \OCP\Util::FATAL);
|
|
return false;
|
|
}
|
|
|
|
// Save the recrypted key to it's owner's keyfiles directory
|
|
// Save new sharekeys to all necessary user directory
|
|
if (
|
|
!Keymanager::setFileKey($this->view, $this, $filePath, $multiEncKey['data'])
|
|
|| !Keymanager::setShareKeys($this->view, $this, $filePath, $multiEncKey['keys'])
|
|
) {
|
|
|
|
\OCP\Util::writeLog('Encryption library',
|
|
'Keyfiles could not be saved for users sharing ' . $filePath, \OCP\Util::ERROR);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// Return proxy to original status
|
|
\OC_FileProxy::$enabled = $proxyStatus;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Find, sanitise and format users sharing a file
|
|
* @note This wraps other methods into a portable bundle
|
|
* @param boolean $sharingEnabled
|
|
* @param string $filePath path relativ to current users files folder
|
|
*/
|
|
public function getSharingUsersArray($sharingEnabled, $filePath) {
|
|
|
|
$appConfig = \OC::$server->getAppConfig();
|
|
|
|
// Check if key recovery is enabled
|
|
if (
|
|
$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);
|
|
|
|
$ownerPath = Helper::stripPartialFileExtension($ownerPath);
|
|
|
|
// always add owner to the list of users with access to the file
|
|
$userIds = array($owner);
|
|
|
|
if ($sharingEnabled) {
|
|
|
|
// Find out who, if anyone, is sharing the file
|
|
$result = \OCP\Share::getUsersSharingFile($ownerPath, $owner);
|
|
$userIds = \array_merge($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 = $appConfig->getValue('files_encryption', 'recoveryKeyId');
|
|
// Add recoveryAdmin to list of users sharing
|
|
$userIds[] = $recoveryKeyId;
|
|
}
|
|
|
|
// check if it is a group mount
|
|
if (\OCP\App::isEnabled("files_external")) {
|
|
$mounts = \OC_Mount_Config::getSystemMountPoints();
|
|
foreach ($mounts as $mount) {
|
|
if ($mount['mountpoint'] == substr($ownerPath, 1, strlen($mount['mountpoint']))) {
|
|
$userIds = array_merge($userIds, $this->getUserWithAccessToMountPoint($mount['applicable']['users'], $mount['applicable']['groups']));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove duplicate UIDs
|
|
$uniqueUserIds = array_unique($userIds);
|
|
|
|
return $uniqueUserIds;
|
|
|
|
}
|
|
|
|
private function getUserWithAccessToMountPoint($users, $groups) {
|
|
$result = array();
|
|
if (in_array('all', $users)) {
|
|
$result = \OCP\User::getUsers();
|
|
} else {
|
|
$result = array_merge($result, $users);
|
|
foreach ($groups as $group) {
|
|
$result = array_merge($result, \OC_Group::usersInGroup($group));
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* set migration status
|
|
* @param int $status
|
|
* @param int $preCondition only update migration status if the previous value equals $preCondition
|
|
* @return boolean
|
|
*/
|
|
private function setMigrationStatus($status, $preCondition = null) {
|
|
|
|
// convert to string if preCondition is set
|
|
$preCondition = ($preCondition === null) ? null : (string)$preCondition;
|
|
|
|
try {
|
|
\OC::$server->getConfig()->setUserValue($this->userId, 'files_encryption', 'migration_status', (string)$status, $preCondition);
|
|
return true;
|
|
} catch(\OCP\PreConditionNotMetException $e) {
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* start migration mode to initially encrypt users data
|
|
* @return boolean
|
|
*/
|
|
public function beginMigration() {
|
|
|
|
$result = $this->setMigrationStatus(self::MIGRATION_IN_PROGRESS, self::MIGRATION_OPEN);
|
|
|
|
if ($result) {
|
|
\OCP\Util::writeLog('Encryption library', "Start migration to encryption mode for " . $this->userId, \OCP\Util::INFO);
|
|
} else {
|
|
\OCP\Util::writeLog('Encryption library', "Could not activate migration mode for " . $this->userId . ". Probably another process already started the initial encryption", \OCP\Util::WARN);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function resetMigrationStatus() {
|
|
return $this->setMigrationStatus(self::MIGRATION_OPEN);
|
|
|
|
}
|
|
|
|
/**
|
|
* close migration mode after users data has been encrypted successfully
|
|
* @return boolean
|
|
*/
|
|
public function finishMigration() {
|
|
$result = $this->setMigrationStatus(self::MIGRATION_COMPLETED);
|
|
|
|
if ($result) {
|
|
\OCP\Util::writeLog('Encryption library', "Finish migration successfully for " . $this->userId, \OCP\Util::INFO);
|
|
} else {
|
|
\OCP\Util::writeLog('Encryption library', "Could not deactivate migration mode for " . $this->userId, \OCP\Util::WARN);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* check if files are already migrated to the encryption system
|
|
* @return int|false migration status, false = in case of 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() {
|
|
|
|
$migrationStatus = false;
|
|
if (\OCP\User::userExists($this->userId)) {
|
|
$migrationStatus = \OC::$server->getConfig()->getUserValue($this->userId, 'files_encryption', 'migration_status', null);
|
|
if ($migrationStatus === null) {
|
|
\OC::$server->getConfig()->setUserValue($this->userId, 'files_encryption', 'migration_status', (string)self::MIGRATION_OPEN);
|
|
$migrationStatus = self::MIGRATION_OPEN;
|
|
}
|
|
}
|
|
|
|
return (int)$migrationStatus;
|
|
|
|
}
|
|
|
|
/**
|
|
* get uid of the owners of the file and the path to the file
|
|
* @param string $path Path of the file to check
|
|
* @throws \Exception
|
|
* @note $shareFilePath must be relative to data/UID/files. Files
|
|
* relative to /Shared are also acceptable
|
|
* @return array
|
|
*/
|
|
public function getUidAndFilename($path) {
|
|
|
|
$pathinfo = pathinfo($path);
|
|
$partfile = false;
|
|
$parentFolder = false;
|
|
if (array_key_exists('extension', $pathinfo) && $pathinfo['extension'] === 'part') {
|
|
// if the real file exists we check this file
|
|
$filePath = $this->userFilesDir . '/' .$pathinfo['dirname'] . '/' . $pathinfo['filename'];
|
|
if ($this->view->file_exists($filePath)) {
|
|
$pathToCheck = $pathinfo['dirname'] . '/' . $pathinfo['filename'];
|
|
} else { // otherwise we look for the parent
|
|
$pathToCheck = $pathinfo['dirname'];
|
|
$parentFolder = true;
|
|
}
|
|
$partfile = true;
|
|
} else {
|
|
$pathToCheck = $path;
|
|
}
|
|
|
|
$view = new \OC\Files\View($this->userFilesDir);
|
|
$fileOwnerUid = $view->getOwner($pathToCheck);
|
|
|
|
// handle public access
|
|
if ($this->isPublic) {
|
|
return array($this->userId, $path);
|
|
} 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($pathToCheck);
|
|
$ownerView = new \OC\Files\View('/' . $fileOwnerUid . '/files');
|
|
|
|
// Fetch real file path from DB
|
|
$filename = $ownerView->getPath($info['fileid']);
|
|
if ($parentFolder) {
|
|
$filename = $filename . '/'. $pathinfo['filename'];
|
|
}
|
|
|
|
if ($partfile) {
|
|
$filename = $filename . '.' . $pathinfo['extension'];
|
|
}
|
|
|
|
}
|
|
|
|
return array(
|
|
$fileOwnerUid,
|
|
\OC\Files\Filesystem::normalizePath($filename)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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, $mountPoint = '') {
|
|
$result = array();
|
|
$dirList = array($dir);
|
|
|
|
while ($dirList) {
|
|
$dir = array_pop($dirList);
|
|
$content = $this->view->getDirectoryContent(\OC\Files\Filesystem::normalizePath(
|
|
$this->userFilesDir . '/' . $dir));
|
|
|
|
foreach ($content as $c) {
|
|
// getDirectoryContent() returns the paths relative to the mount points, so we need
|
|
// to re-construct the complete path
|
|
$path = ($mountPoint !== '') ? $mountPoint . '/' . $c['path'] : $c['path'];
|
|
$path = \OC\Files\Filesystem::normalizePath($path);
|
|
if ($c['type'] === 'dir') {
|
|
$dirList[] = substr($path, strlen('/' . \OCP\User::getUser() . "/files"));
|
|
} else {
|
|
$result[] = substr($path, strlen('/' . \OCP\User::getUser() . "/files"));
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* get owner of the shared files.
|
|
* @param int $id ID of a share
|
|
* @return string owner
|
|
*/
|
|
public function getOwnerFromSharedFile($id) {
|
|
|
|
$query = \OCP\DB::prepare('SELECT `parent`, `uid_owner` FROM `*PREFIX*share` WHERE `id` = ?', 1);
|
|
|
|
$result = $query->execute(array($id));
|
|
|
|
$source = null;
|
|
if (\OCP\DB::isError($result)) {
|
|
\OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR);
|
|
} else {
|
|
$source = $result->fetchRow();
|
|
}
|
|
|
|
$fileOwner = false;
|
|
|
|
if ($source && isset($source['parent'])) {
|
|
|
|
$parent = $source['parent'];
|
|
|
|
while (isset($parent)) {
|
|
|
|
$query = \OCP\DB::prepare('SELECT `parent`, `uid_owner` FROM `*PREFIX*share` WHERE `id` = ?', 1);
|
|
|
|
$result = $query->execute(array($parent));
|
|
|
|
$item = null;
|
|
if (\OCP\DB::isError($result)) {
|
|
\OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR);
|
|
} else {
|
|
$item = $result->fetchRow();
|
|
}
|
|
|
|
if ($item && 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 getKeyId() {
|
|
return $this->keyId;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getUserFilesDir() {
|
|
return $this->userFilesDir;
|
|
}
|
|
|
|
/**
|
|
* @param string $password
|
|
* @return bool
|
|
*/
|
|
public function checkRecoveryPassword($password) {
|
|
|
|
$result = false;
|
|
|
|
$recoveryKey = Keymanager::getPrivateSystemKey($this->recoveryKeyId);
|
|
$decryptedRecoveryKey = Crypt::decryptPrivateKey($recoveryKey, $password);
|
|
|
|
if ($decryptedRecoveryKey) {
|
|
$result = true;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getRecoveryKeyId() {
|
|
return $this->recoveryKeyId;
|
|
}
|
|
|
|
/**
|
|
* add recovery key to all encrypted files
|
|
*/
|
|
public function addRecoveryKeys($path = '/') {
|
|
$dirContent = $this->view->getDirectoryContent($this->keysPath . '/' . $path);
|
|
foreach ($dirContent as $item) {
|
|
// get relative path from files_encryption/keyfiles/
|
|
$filePath = substr($item['path'], strlen('files_encryption/keys'));
|
|
if ($this->view->is_dir($this->userFilesDir . '/' . $filePath)) {
|
|
$this->addRecoveryKeys($filePath . '/');
|
|
} else {
|
|
$session = new Session(new \OC\Files\View('/'));
|
|
$sharingEnabled = \OCP\Share::isEnabled();
|
|
$usersSharing = $this->getSharingUsersArray($sharingEnabled, $filePath);
|
|
$this->setSharedFileKeyfiles($session, $usersSharing, $filePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* remove recovery key to all encrypted files
|
|
*/
|
|
public function removeRecoveryKeys($path = '/') {
|
|
$dirContent = $this->view->getDirectoryContent($this->keysPath . '/' . $path);
|
|
foreach ($dirContent as $item) {
|
|
// get relative path from files_encryption/keyfiles
|
|
$filePath = substr($item['path'], strlen('files_encryption/keys'));
|
|
if ($this->view->is_dir($this->userFilesDir . '/' . $filePath)) {
|
|
$this->removeRecoveryKeys($filePath . '/');
|
|
} else {
|
|
$this->view->unlink($this->keysPath . '/' . $filePath . '/' . $this->recoveryKeyId . '.shareKey');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
$userIds = $result['users'];
|
|
$userIds[] = $this->recoveryKeyId;
|
|
if ($result['public']) {
|
|
$userIds[] = $this->publicShareKeyId;
|
|
}
|
|
} else {
|
|
$userIds = array(
|
|
$this->userId,
|
|
$this->recoveryKeyId
|
|
);
|
|
}
|
|
$filteredUids = $this->filterShareReadyUsers($userIds);
|
|
|
|
//decrypt file key
|
|
$encKeyfile = Keymanager::getFileKey($this->view, $this, $file);
|
|
$shareKey = Keymanager::getShareKey($this->view, $this->recoveryKeyId, $this, $file);
|
|
$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);
|
|
|
|
Keymanager::setFileKey($this->view, $this, $file, $multiEncKey['data']);
|
|
Keymanager::setShareKeys($this->view, $this, $file, $multiEncKey['keys']);
|
|
|
|
}
|
|
|
|
/**
|
|
* 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->keysPath . '/' . $path);
|
|
foreach ($dirContent as $item) {
|
|
// get relative path from files_encryption/keyfiles
|
|
$filePath = substr($item['path'], strlen('files_encryption/keys'));
|
|
if ($this->view->is_dir($this->userFilesDir . '/' . $filePath)) {
|
|
$this->recoverAllFiles($filePath . '/', $privateKey);
|
|
} else {
|
|
$this->recoverFile($filePath, $privateKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* recover users files in case of password lost
|
|
* @param string $recoveryPassword
|
|
*/
|
|
public function recoverUsersFiles($recoveryPassword) {
|
|
|
|
$encryptedKey = Keymanager::getPrivateSystemKey( $this->recoveryKeyId);
|
|
$privateKey = Crypt::decryptPrivateKey($encryptedKey, $recoveryPassword);
|
|
|
|
$this->recoverAllFiles('/', $privateKey);
|
|
}
|
|
|
|
/**
|
|
* create a backup of all keys from the user
|
|
*
|
|
* @param string $purpose define the purpose of the backup, will be part of the backup folder name
|
|
* @param boolean $timestamp (optional) should a timestamp be added, default true
|
|
* @param boolean $includeUserKeys (optional) include users private-/public-key, default true
|
|
*/
|
|
public function backupAllKeys($purpose, $timestamp = true, $includeUserKeys = true) {
|
|
$this->userId;
|
|
$backupDir = $this->encryptionDir . '/backup.' . $purpose;
|
|
$backupDir .= ($timestamp) ? '.' . date("Y-m-d_H-i-s") . '/' : '/';
|
|
$this->view->mkdir($backupDir);
|
|
$this->view->copy($this->keysPath, $backupDir . 'keys/');
|
|
if ($includeUserKeys) {
|
|
$this->view->copy($this->privateKeyPath, $backupDir . $this->userId . '.privateKey');
|
|
$this->view->copy($this->publicKeyPath, $backupDir . $this->userId . '.publicKey');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* restore backup
|
|
*
|
|
* @param string $backup complete name of the backup
|
|
* @return boolean
|
|
*/
|
|
public function restoreBackup($backup) {
|
|
$backupDir = $this->encryptionDir . '/backup.' . $backup . '/';
|
|
|
|
$fileKeysRestored = $this->view->rename($backupDir . 'keys', $this->encryptionDir . '/keys');
|
|
|
|
$pubKeyRestored = $privKeyRestored = true;
|
|
if (
|
|
$this->view->file_exists($backupDir . $this->userId . '.privateKey') &&
|
|
$this->view->file_exists($backupDir . $this->userId . '.privateKey')
|
|
) {
|
|
|
|
$pubKeyRestored = $this->view->rename($backupDir . $this->userId . '.publicKey', $this->publicKeyPath);
|
|
$privKeyRestored = $this->view->rename($backupDir . $this->userId . '.privateKey', $this->privateKeyPath);
|
|
}
|
|
|
|
if ($fileKeysRestored && $pubKeyRestored && $privKeyRestored) {
|
|
$this->view->deleteAll($backupDir);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* delete backup
|
|
*
|
|
* @param string $backup complete name of the backup
|
|
* @return boolean
|
|
*/
|
|
public function deleteBackup($backup) {
|
|
$backupDir = $this->encryptionDir . '/backup.' . $backup . '/';
|
|
return $this->view->deleteAll($backupDir);
|
|
}
|
|
|
|
/**
|
|
* check if the file is stored on a system wide mount point
|
|
* @param string $path relative to /data/user with leading '/'
|
|
* @return boolean
|
|
*/
|
|
public function isSystemWideMountPoint($path) {
|
|
$normalizedPath = ltrim($path, '/');
|
|
if (\OCP\App::isEnabled("files_external")) {
|
|
$mounts = \OC_Mount_Config::getSystemMountPoints();
|
|
foreach ($mounts as $mount) {
|
|
if ($mount['mountpoint'] == substr($normalizedPath, 0, strlen($mount['mountpoint']))) {
|
|
if ($this->isMountPointApplicableToUser($mount)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* check if mount point is applicable to user
|
|
*
|
|
* @param array $mount contains $mount['applicable']['users'], $mount['applicable']['groups']
|
|
* @return boolean
|
|
*/
|
|
protected function isMountPointApplicableToUser($mount) {
|
|
$uid = \OCP\User::getUser();
|
|
$acceptedUids = array('all', $uid);
|
|
// check if mount point is applicable for the user
|
|
$intersection = array_intersect($acceptedUids, $mount['applicable']['users']);
|
|
if (!empty($intersection)) {
|
|
return true;
|
|
}
|
|
// check if mount point is applicable for group where the user is a member
|
|
foreach ($mount['applicable']['groups'] as $gid) {
|
|
if (\OC_Group::inGroup($uid, $gid)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* decrypt private key and add it to the current session
|
|
* @param array $params with 'uid' and 'password'
|
|
* @return mixed session or false
|
|
*/
|
|
public function initEncryption($params) {
|
|
|
|
$session = new Session($this->view);
|
|
|
|
// we tried to initialize the encryption app for this session
|
|
$session->setInitialized(Session::INIT_EXECUTED);
|
|
|
|
$encryptedKey = Keymanager::getPrivateKey($this->view, $params['uid']);
|
|
|
|
$privateKey = false;
|
|
if ($encryptedKey) {
|
|
$privateKey = Crypt::decryptPrivateKey($encryptedKey, $params['password']);
|
|
}
|
|
|
|
if ($privateKey === false) {
|
|
\OCP\Util::writeLog('Encryption library', 'Private key for user "' . $params['uid']
|
|
. '" is not valid! Maybe the user password was changed from outside if so please change it back to gain access', \OCP\Util::ERROR);
|
|
return false;
|
|
}
|
|
|
|
$session->setPrivateKey($privateKey);
|
|
$session->setInitialized(Session::INIT_SUCCESSFUL);
|
|
|
|
return $session;
|
|
}
|
|
|
|
/*
|
|
* remove encryption related keys from the session
|
|
*/
|
|
public function closeEncryptionSession() {
|
|
$session = new Session($this->view);
|
|
$session->closeSession();
|
|
}
|
|
|
|
}
|