1806 lines
47 KiB
PHP
1806 lines
47 KiB
PHP
<?php
|
||
/**
|
||
* ownCloud
|
||
*
|
||
* @author Sam Tuke, Frank Karlitschek
|
||
* @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
|
||
* 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/>.
|
||
*
|
||
*/
|
||
|
||
# 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"
|
||
|
||
namespace OCA\Encryption;
|
||
|
||
/**
|
||
* @brief Class for utilities relating to encrypted file storage system
|
||
* @param \OC_FilesystemView $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 {
|
||
|
||
// 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: implement flag system to allow user to specify encryption by folder, subfolder, etc.
|
||
|
||
|
||
// Integration testing:
|
||
|
||
//// TODO: test new encryption with versioning
|
||
//// DONE: test new encryption with sharing
|
||
//// TODO: test new encryption with proxies
|
||
|
||
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
|
||
|
||
|
||
private $view; // OC_FilesystemView object for filesystem operations
|
||
private $userId; // ID of the currently logged-in user
|
||
private $client; // Client side encryption mode flag
|
||
private $publicKeyDir; // Dir containing all public user keys
|
||
private $encryptionDir; // Dir containing user's files_encryption
|
||
private $keyfilesPath; // Dir containing user's keyfiles
|
||
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->isPublic = false;
|
||
|
||
$this->publicShareKeyId = \OC_Appconfig::getValue('files_encryption', 'publicShareKeyId');
|
||
$this->recoveryKeyId = \OC_Appconfig::getValue('files_encryption', 'recoveryKeyId');
|
||
|
||
// if we are anonymous/public
|
||
if (\OCA\Encryption\Helper::isPublicAccess()) {
|
||
$this->userId = $this->publicShareKeyId;
|
||
|
||
// only handle for files_sharing app
|
||
if (isset($GLOBALS['app']) && $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)
|
||
) {
|
||
|
||
return false;
|
||
|
||
} else {
|
||
|
||
return true;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* @brief 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->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
|
||
// 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);
|
||
|
||
// Save key-pair
|
||
if ($encryptedPrivateKey) {
|
||
$this->view->file_put_contents($this->privateKeyPath, $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->userId . '"', \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->userId . '"', \OCP\Util::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();
|
||
|
||
if (\OCP\DB::isError($result)) {
|
||
\OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR);
|
||
} else {
|
||
if ($result->numRows() > 0) {
|
||
$row = $result->fetchRow();
|
||
if (isset($row['recovery_enabled'])) {
|
||
$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
|
||
);
|
||
|
||
}
|
||
|
||
return is_numeric(\OC_DB::executeAudited($sql, $args));
|
||
|
||
}
|
||
|
||
/**
|
||
* @brief 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 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 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(),
|
||
'legacy' => array()
|
||
);
|
||
}
|
||
|
||
if (
|
||
$this->view->is_dir($directory)
|
||
&& $handle = $this->view->opendir($directory)
|
||
) {
|
||
|
||
while (false !== ($file = readdir($handle))) {
|
||
|
||
if (
|
||
$file !== "."
|
||
&& $file !== ".."
|
||
) {
|
||
|
||
$filePath = $directory . '/' . $this->view->getRelativePath('/' . $file);
|
||
$relPath = \OCA\Encryption\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 (
|
||
Keymanager::getFileKey($this->view, $this->userId, $relPath)
|
||
&& $isEncryptedPath
|
||
) {
|
||
|
||
$found['encrypted'][] = array(
|
||
'name' => $file,
|
||
'path' => $filePath
|
||
);
|
||
|
||
// If the file uses old
|
||
// encryption system
|
||
} elseif (Crypt::isLegacyEncryptedContent($isEncryptedPath, $relPath)) {
|
||
|
||
$found['legacy'][] = array(
|
||
'name' => $file,
|
||
'path' => $filePath
|
||
);
|
||
|
||
// If the file is not encrypted
|
||
} else {
|
||
|
||
$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 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 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;
|
||
|
||
// we only need 24 byte from the last chunk
|
||
$data = '';
|
||
$handle = $this->view->fopen($path, 'r');
|
||
if (is_resource($handle) && !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;
|
||
|
||
// split the path parts
|
||
$pathParts = explode('/', $path);
|
||
|
||
if (isset($pathParts[2]) && $pathParts[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://' . $path, "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 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;
|
||
}
|
||
|
||
|
||
/**
|
||
* @param $path
|
||
* @return bool
|
||
*/
|
||
public function isSharedPath($path) {
|
||
|
||
$trimmed = ltrim($path, '/');
|
||
$split = explode('/', $trimmed);
|
||
|
||
if (isset($split[2]) && $split[2] === 'Shared') {
|
||
|
||
return true;
|
||
|
||
} else {
|
||
|
||
return false;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* @brief 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;
|
||
}
|
||
|
||
/**
|
||
* @brief 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;
|
||
}
|
||
|
||
/**
|
||
* @brief 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();
|
||
|
||
// Disable proxy to prevent file being encrypted twice
|
||
\OC_FileProxy::$enabled = false;
|
||
|
||
// Encrypt unencrypted files
|
||
foreach ($found['encrypted'] as $encryptedFile) {
|
||
|
||
//relative to data/<user>/file
|
||
$relPath = Helper::stripUserFilesPath($encryptedFile['path']);
|
||
|
||
//relative to /data
|
||
$rawPath = $encryptedFile['path'];
|
||
|
||
// Open enc file handle for binary reading
|
||
$encHandle = fopen('crypt://' . $rawPath, 'rb');
|
||
|
||
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);
|
||
|
||
$this->view->chroot($fakeRoot);
|
||
|
||
// Add the file to the cache
|
||
\OC\Files\Filesystem::putFileInfo($relPath, array(
|
||
'encrypted' => false,
|
||
'size' => $size,
|
||
'unencrypted_size' => $size
|
||
));
|
||
|
||
$decryptedFiles[] = $relPath;
|
||
|
||
}
|
||
|
||
if ($versionStatus) {
|
||
\OC_App::enable('files_versions');
|
||
}
|
||
|
||
if (!$this->decryptVersions($decryptedFiles)) {
|
||
$successful = false;
|
||
}
|
||
|
||
if ($successful) {
|
||
$this->view->deleteAll($this->keyfilesPath);
|
||
$this->view->deleteAll($this->shareKeysPath);
|
||
}
|
||
|
||
\OC_FileProxy::$enabled = true;
|
||
}
|
||
|
||
return $successful;
|
||
}
|
||
|
||
/**
|
||
* @brief Encrypt all files in a directory
|
||
* @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($dirPath, $legacyPassphrase = null, $newPassphrase = null) {
|
||
|
||
$found = $this->findEncFiles($dirPath);
|
||
|
||
if ($found) {
|
||
|
||
// 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) {
|
||
|
||
//relative to data/<user>/file
|
||
$relPath = $plainFile['path'];
|
||
|
||
//relative to /data
|
||
$rawPath = '/' . $this->userId . '/files/' . $plainFile['path'];
|
||
|
||
// 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');
|
||
|
||
// 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);
|
||
|
||
$this->view->chroot($fakeRoot);
|
||
|
||
// Add the file to the cache
|
||
\OC\Files\Filesystem::putFileInfo($relPath, array(
|
||
'encrypted' => true,
|
||
'size' => $size,
|
||
'unencrypted_size' => $size
|
||
));
|
||
|
||
$encryptedFiles[] = $relPath;
|
||
|
||
}
|
||
|
||
// Encrypt legacy encrypted files
|
||
if (
|
||
!empty($legacyPassphrase)
|
||
&& !empty($newPassphrase)
|
||
) {
|
||
|
||
foreach ($found['legacy'] as $legacyFile) {
|
||
|
||
// Fetch data from file
|
||
$legacyData = $this->view->file_get_contents($legacyFile['path']);
|
||
|
||
// decrypt data, generate catfile
|
||
$decrypted = Crypt::legacyBlockDecrypt($legacyData, $legacyPassphrase);
|
||
|
||
$rawPath = $legacyFile['path'];
|
||
|
||
// enable proxy the ensure encryption is handled
|
||
\OC_FileProxy::$enabled = true;
|
||
|
||
// Open enc file handle for binary writing, with same filename as original plain file
|
||
$encHandle = $this->view->fopen( $rawPath, 'wb' );
|
||
|
||
if (is_resource($encHandle)) {
|
||
|
||
// write data to stream
|
||
fwrite($encHandle, $decrypted);
|
||
|
||
// close stream
|
||
fclose($encHandle);
|
||
}
|
||
|
||
// disable proxy to prevent file being encrypted twice
|
||
\OC_FileProxy::$enabled = false;
|
||
}
|
||
}
|
||
|
||
\OC_FileProxy::$enabled = true;
|
||
|
||
if ($versionStatus) {
|
||
\OC_App::enable('files_versions');
|
||
}
|
||
|
||
$this->encryptVersions($encryptedFiles);
|
||
|
||
// 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) {
|
||
|
||
$sql = 'SELECT `path` FROM `*PREFIX*filecache` WHERE `fileid` = ?';
|
||
|
||
$query = \OCP\DB::prepare($sql);
|
||
|
||
$result = $query->execute(array($fileId));
|
||
|
||
$path = false;
|
||
if (\OCP\DB::isError($result)) {
|
||
\OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR);
|
||
} else {
|
||
if ($result->numRows() > 0) {
|
||
$row = $result->fetchRow();
|
||
$path = substr($row['path'], strlen('files'));
|
||
}
|
||
}
|
||
|
||
return $path;
|
||
|
||
}
|
||
|
||
/**
|
||
* @brief 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) {
|
||
|
||
$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
|
||
\OCP\Util::writeLog('Encryption library',
|
||
'"' . $user . '" is not setup for encryption', \OCP\Util::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
|
||
* @return bool|string
|
||
* @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'])) {
|
||
|
||
\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();
|
||
|
||
$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'])
|
||
) {
|
||
|
||
\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;
|
||
}
|
||
|
||
/**
|
||
* @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);
|
||
$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;
|
||
}
|
||
|
||
// check if it is a group mount
|
||
if (\OCP\App::isEnabled("files_external")) {
|
||
$mount = \OC_Mount_Config::getSystemMountPoints();
|
||
foreach ($mount as $mountPoint => $data) {
|
||
if ($mountPoint == substr($ownerPath, 1, strlen($mountPoint))) {
|
||
$userIds = array_merge($userIds, $this->getUserWithAccessToMountPoint($data['applicable']['users'], $data['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;
|
||
}
|
||
|
||
/**
|
||
* @brief start migration mode to initially encrypt users data
|
||
* @return boolean
|
||
*/
|
||
public function beginMigration() {
|
||
|
||
$return = false;
|
||
|
||
$sql = 'UPDATE `*PREFIX*encryption` SET `migration_status` = ? WHERE `uid` = ? and `migration_status` = ?';
|
||
$args = array(self::MIGRATION_IN_PROGRESS, $this->userId, self::MIGRATION_OPEN);
|
||
$query = \OCP\DB::prepare($sql);
|
||
$manipulatedRows = $query->execute($args);
|
||
|
||
if ($manipulatedRows === 1) {
|
||
$return = true;
|
||
\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 $return;
|
||
}
|
||
|
||
/**
|
||
* @brief close migration mode after users data has been encrypted successfully
|
||
* @return boolean
|
||
*/
|
||
public function finishMigration() {
|
||
|
||
$return = false;
|
||
|
||
$sql = 'UPDATE `*PREFIX*encryption` SET `migration_status` = ? WHERE `uid` = ? and `migration_status` = ?';
|
||
$args = array(self::MIGRATION_COMPLETED, $this->userId, self::MIGRATION_IN_PROGRESS);
|
||
$query = \OCP\DB::prepare($sql);
|
||
$manipulatedRows = $query->execute($args);
|
||
|
||
if ($manipulatedRows === 1) {
|
||
$return = true;
|
||
\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 $return;
|
||
}
|
||
|
||
/**
|
||
* @brief check if files are already migrated to the encryption system
|
||
* @return 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() {
|
||
|
||
$sql = 'SELECT `migration_status` FROM `*PREFIX*encryption` WHERE `uid` = ?';
|
||
|
||
$args = array($this->userId);
|
||
|
||
$query = \OCP\DB::prepare($sql);
|
||
|
||
$result = $query->execute($args);
|
||
|
||
$migrationStatus = array();
|
||
|
||
if (\OCP\DB::isError($result)) {
|
||
\OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR);
|
||
} else {
|
||
if ($result->numRows() > 0) {
|
||
$row = $result->fetchRow();
|
||
if (isset($row['migration_status'])) {
|
||
$migrationStatus[] = $row['migration_status'];
|
||
}
|
||
}
|
||
}
|
||
|
||
// If no record is found
|
||
if (empty($migrationStatus)) {
|
||
\OCP\Util::writeLog('Encryption library', "Could not get migration status for " . $this->userId . ", no record found", \OCP\Util::ERROR);
|
||
return false;
|
||
// If a record is found
|
||
} else {
|
||
return (int)$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
|
||
* @throws \Exception
|
||
* @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,
|
||
\OC_Filesystem::normalizePath($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(\OC\Files\Filesystem::normalizePath(
|
||
$this->userFilesDir . '/' . $dir));
|
||
|
||
// handling for re shared folders
|
||
$pathSplit = explode('/', $dir);
|
||
|
||
foreach ($content as $c) {
|
||
|
||
$sharedPart = $pathSplit[sizeof($pathSplit) - 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) {
|
||
|
||
$sql = 'SELECT `file_target`, `item_type` FROM `*PREFIX*share` WHERE `id` = ?';
|
||
|
||
$query = \OCP\DB::prepare($sql);
|
||
|
||
$result = $query->execute(array($id));
|
||
|
||
$row = array();
|
||
if (\OCP\DB::isError($result)) {
|
||
\OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR);
|
||
} else {
|
||
if ($result->numRows() > 0) {
|
||
$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) {
|
||
|
||
$sql = 'SELECT `parent` FROM `*PREFIX*share` WHERE `id` = ?';
|
||
|
||
$query = \OCP\DB::prepare($sql);
|
||
|
||
$result = $query->execute(array($id));
|
||
|
||
$row = array();
|
||
if (\OCP\DB::isError($result)) {
|
||
\OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR);
|
||
} else {
|
||
if ($result->numRows() > 0) {
|
||
$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 = \OCP\DB::prepare('SELECT `parent`, `uid_owner` FROM `*PREFIX*share` WHERE `id` = ?', 1);
|
||
|
||
$result = $query->execute(array($id));
|
||
|
||
$source = array();
|
||
if (\OCP\DB::isError($result)) {
|
||
\OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR);
|
||
} else {
|
||
if ($result->numRows() > 0) {
|
||
$source = $result->fetchRow();
|
||
}
|
||
}
|
||
|
||
$fileOwner = false;
|
||
|
||
if (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 = array();
|
||
if (\OCP\DB::isError($result)) {
|
||
\OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR);
|
||
} else {
|
||
if ($result->numRows() > 0) {
|
||
$item = $result->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) {
|
||
|
||
$result = false;
|
||
$pathKey = '/owncloud_private_key/' . $this->recoveryKeyId . ".private.key";
|
||
|
||
$proxyStatus = \OC_FileProxy::$enabled;
|
||
\OC_FileProxy::$enabled = false;
|
||
|
||
$recoveryKey = $this->view->file_get_contents($pathKey);
|
||
|
||
$decryptedRecoveryKey = Crypt::decryptPrivateKey($recoveryKey, $password);
|
||
|
||
if ($decryptedRecoveryKey) {
|
||
$result = true;
|
||
}
|
||
|
||
\OC_FileProxy::$enabled = $proxyStatus;
|
||
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* @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 \OCA\Encryption\Session(new \OC_FilesystemView('/'));
|
||
$sharingEnabled = \OCP\Share::isEnabled();
|
||
// remove '.key' extension from path e.g. 'file.txt.key' to 'file.txt'
|
||
$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 {
|
||
// remove '.key' extension from path e.g. 'file.txt.key' to 'file.txt'
|
||
$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);
|
||
$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) {
|
||
// get relative path from files_encryption/keyfiles
|
||
$filePath = substr($item['path'], strlen('files_encryption/keyfiles'));
|
||
if ($item['type'] === 'dir') {
|
||
$this->recoverAllFiles($filePath . '/', $privateKey);
|
||
} else {
|
||
// remove '.key' extension from path e.g. 'file.txt.key' to 'file.txt'
|
||
$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::decryptPrivateKey($encryptedKey, $recoveryPassword);
|
||
|
||
\OC_FileProxy::$enabled = $proxyStatus;
|
||
|
||
$this->recoverAllFiles('/', $privateKey);
|
||
}
|
||
|
||
/**
|
||
* Get the path including the storage mount point
|
||
* @param int $id
|
||
* @return string the path including the mount point like AmazonS3/folder/file.txt
|
||
*/
|
||
public function getPathWithMountPoint($id) {
|
||
list($storage, $internalPath) = \OC\Files\Cache\Cache::getById($id);
|
||
$mount = \OC\Files\Filesystem::getMountByStorageId($storage);
|
||
$mountPoint = $mount[0]->getMountPoint();
|
||
$path = \OC\Files\Filesystem::normalizePath($mountPoint . '/' . $internalPath);
|
||
|
||
// reformat the path to be relative e.g. /user/files/folder becomes /folder/
|
||
$relativePath = \OCA\Encryption\Helper::stripUserFilesPath($path);
|
||
|
||
return $relativePath;
|
||
}
|
||
|
||
/**
|
||
* @brief check if the file is stored on a system wide mount point
|
||
* @param $path relative to /data/user with leading '/'
|
||
* @return boolean
|
||
*/
|
||
public function isSystemWideMountPoint($path) {
|
||
if (\OCP\App::isEnabled("files_external")) {
|
||
$mount = \OC_Mount_Config::getSystemMountPoints();
|
||
foreach ($mount as $mountPoint => $data) {
|
||
if ($mountPoint == substr($path, 1, strlen($mountPoint))) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* @brief 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) {
|
||
|
||
$encryptedKey = Keymanager::getPrivateKey($this->view, $params['uid']);
|
||
|
||
$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 = new \OCA\Encryption\Session($this->view);
|
||
|
||
$session->setPrivateKey($privateKey);
|
||
|
||
return $session;
|
||
}
|
||
|
||
}
|