diff options
author | Lukas Reschke <lukas@statuscode.ch> | 2014-03-20 19:31:12 +0400 |
---|---|---|
committer | Lukas Reschke <lukas@statuscode.ch> | 2014-03-20 19:31:12 +0400 |
commit | 2c561c9c5072ce82e06bd5ab2c4ee81bc5d09d59 (patch) | |
tree | a34586bf389cc8011490031bba497129361655a3 /apps | |
parent | 316a22b46315a389e38cd9f1e320f3e1f0541d68 (diff) | |
parent | d95fde39248e8c4083c14788fe3a327a60611b47 (diff) |
Merge pull request #7794 from owncloud/extstorage-obfuscatepasswords
Obfuscate passwords in ext storage config
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files_external/appinfo/app.php | 3 | ||||
-rwxr-xr-x | apps/files_external/lib/config.php | 188 | ||||
-rw-r--r-- | apps/files_external/lib/smb.php | 2 | ||||
-rw-r--r-- | apps/files_external/tests/mountconfig.php | 309 |
4 files changed, 486 insertions, 16 deletions
diff --git a/apps/files_external/appinfo/app.php b/apps/files_external/appinfo/app.php index 5b1cd86a170..0e83660f845 100644 --- a/apps/files_external/appinfo/app.php +++ b/apps/files_external/appinfo/app.php @@ -25,5 +25,6 @@ if (OCP\Config::getAppValue('files_external', 'allow_user_mounting', 'yes') == ' } // connecting hooks -OCP\Util::connectHook( 'OC_User', 'post_login', 'OC\Files\Storage\iRODS', 'login' ); +OCP\Util::connectHook('OC_Filesystem', 'post_initMountPoints', '\OC_Mount_Config', 'initMountPointsHook'); +OCP\Util::connectHook('OC_User', 'post_login', 'OC\Files\Storage\iRODS', 'login'); diff --git a/apps/files_external/lib/config.php b/apps/files_external/lib/config.php index 2c8828c4d51..ffbab7bca80 100755 --- a/apps/files_external/lib/config.php +++ b/apps/files_external/lib/config.php @@ -4,6 +4,7 @@ * * @author Michael Gapczynski * @copyright 2012 Michael Gapczynski mtgap@owncloud.com +* @copyright 2014 Vincent Petry <pvince81@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 @@ -19,10 +20,16 @@ * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ +set_include_path( + get_include_path() . PATH_SEPARATOR . + \OC_App::getAppPath('files_external') . '/3rdparty/phpseclib/phpseclib' +); + /** -* Class to configure the config/mount.php and data/$user/mount.php files -*/ + * Class to configure mount.json globally and for users + */ class OC_Mount_Config { + // TODO: make this class non-static and give it a proper namespace const MOUNT_TYPE_GLOBAL = 'global'; const MOUNT_TYPE_GROUP = 'group'; @@ -42,6 +49,7 @@ class OC_Mount_Config { */ public static function getBackends() { + // FIXME: do not rely on php key order for the options order in the UI $backends['\OC\Files\Storage\Local']=array( 'backend' => 'Local', 'configuration' => array( @@ -159,6 +167,96 @@ class OC_Mount_Config { } /** + * Hook that mounts the given user's visible mount points + * @param array $data + */ + public static function initMountPointsHook($data) { + $mountPoints = self::getAbsoluteMountPoints($data['user']); + foreach ($mountPoints as $mountPoint => $options) { + \OC\Files\Filesystem::mount($options['class'], $options['options'], $mountPoint); + } + } + + /** + * Returns the mount points for the given user. + * The mount point is relative to the data directory. + * + * @param string $user user + * @return array of mount point string as key, mountpoint config as value + */ + public static function getAbsoluteMountPoints($user) { + $mountPoints = array(); + + $datadir = \OC_Config::getValue("datadirectory", \OC::$SERVERROOT . "/data"); + $mount_file = \OC_Config::getValue("mount_file", $datadir . "/mount.json"); + + //move config file to it's new position + if (is_file(\OC::$SERVERROOT . '/config/mount.json')) { + rename(\OC::$SERVERROOT . '/config/mount.json', $mount_file); + } + + // Load system mount points + $mountConfig = self::readData(false); + if (isset($mountConfig[self::MOUNT_TYPE_GLOBAL])) { + foreach ($mountConfig[self::MOUNT_TYPE_GLOBAL] as $mountPoint => $options) { + $options['options'] = self::decryptPasswords($options['options']); + $mountPoints[$mountPoint] = $options; + } + } + if (isset($mountConfig[self::MOUNT_TYPE_GROUP])) { + foreach ($mountConfig[self::MOUNT_TYPE_GROUP] as $group => $mounts) { + if (\OC_Group::inGroup($user, $group)) { + foreach ($mounts as $mountPoint => $options) { + $mountPoint = self::setUserVars($user, $mountPoint); + foreach ($options as &$option) { + $option = self::setUserVars($user, $option); + } + $options['options'] = self::decryptPasswords($options['options']); + $mountPoints[$mountPoint] = $options; + } + } + } + } + if (isset($mountConfig[self::MOUNT_TYPE_USER])) { + foreach ($mountConfig[self::MOUNT_TYPE_USER] as $mountUser => $mounts) { + if ($mountUser === 'all' or strtolower($mountUser) === strtolower($user)) { + foreach ($mounts as $mountPoint => $options) { + $mountPoint = self::setUserVars($user, $mountPoint); + foreach ($options as &$option) { + $option = self::setUserVars($user, $option); + } + $options['options'] = self::decryptPasswords($options['options']); + $mountPoints[$mountPoint] = $options; + } + } + } + } + + // Load personal mount points + $mountConfig = self::readData(true); + if (isset($mountConfig[self::MOUNT_TYPE_USER][$user])) { + foreach ($mountConfig[self::MOUNT_TYPE_USER][$user] as $mountPoint => $options) { + $options['options'] = self::decryptPasswords($options['options']); + $mountPoints[$mountPoint] = $options; + } + } + + return $mountPoints; + } + + /** + * fill in the correct values for $user + * + * @param string $user + * @param string $input + * @return string + */ + private static function setUserVars($user, $input) { + return str_replace('$user', $user, $input); + } + + + /** * Get details on each of the external storage backends, used for the mount config UI * Some backends are not available as a personal backend, f.e. Local and such that have * been disabled by the admin. @@ -203,6 +301,7 @@ class OC_Mount_Config { if (strpos($mount['class'], 'OC_Filestorage_') !== false) { $mount['class'] = '\OC\Files\Storage\\'.substr($mount['class'], 15); } + $mount['options'] = self::decryptPasswords($mount['options']); // Remove '/$user/files/' from mount point $mountPoint = substr($mountPoint, 13); // Merge the mount point into the current mount points @@ -228,6 +327,7 @@ class OC_Mount_Config { if (strpos($mount['class'], 'OC_Filestorage_') !== false) { $mount['class'] = '\OC\Files\Storage\\'.substr($mount['class'], 15); } + $mount['options'] = self::decryptPasswords($mount['options']); // Remove '/$user/files/' from mount point $mountPoint = substr($mountPoint, 13); // Merge the mount point into the current mount points @@ -265,6 +365,7 @@ class OC_Mount_Config { if (strpos($mount['class'], 'OC_Filestorage_') !== false) { $mount['class'] = '\OC\Files\Storage\\'.substr($mount['class'], 15); } + $mount['options'] = self::decryptPasswords($mount['options']); // Remove '/uid/files/' from mount point $personal[substr($mountPoint, strlen($uid) + 8)] = array( 'class' => $mount['class'], @@ -277,12 +378,18 @@ class OC_Mount_Config { return $personal; } + /** + * Test connecting using the given backend configuration + * @param string $class backend class name + * @param array $options backend configuration options + * @return bool true if the connection succeeded, false otherwise + */ private static function getBackendStatus($class, $options) { if (self::$skipTest) { return true; } foreach ($options as &$option) { - $option = str_replace('$user', OCP\User::getUser(), $option); + $option = self::setUserVars(OCP\User::getUser(), $option); } if (class_exists($class)) { try { @@ -334,7 +441,13 @@ class OC_Mount_Config { } else { $mountPoint = '/$user/files/'.ltrim($mountPoint, '/'); } - $mount = array($applicable => array($mountPoint => array('class' => $class, 'options' => $classOptions))); + + $mount = array($applicable => array( + $mountPoint => array( + 'class' => $class, + 'options' => self::encryptPasswords($classOptions)) + ) + ); $mountPoints = self::readData($isPersonal); // Merge the new mount point into the current mount points if (isset($mountPoints[$mountType])) { @@ -527,4 +640,71 @@ class OC_Mount_Config { return $txt; } + + /** + * Encrypt passwords in the given config options + * @param array $options mount options + * @return array updated options + */ + private static function encryptPasswords($options) { + if (isset($options['password'])) { + $options['password_encrypted'] = self::encryptPassword($options['password']); + // do not unset the password, we want to keep the keys order + // on load... because that's how the UI currently works + $options['password'] = ''; + } + return $options; + } + + /** + * Decrypt passwords in the given config options + * @param array $options mount options + * @return array updated options + */ + private static function decryptPasswords($options) { + // note: legacy options might still have the unencrypted password in the "password" field + if (isset($options['password_encrypted'])) { + $options['password'] = self::decryptPassword($options['password_encrypted']); + unset($options['password_encrypted']); + } + return $options; + } + + /** + * Encrypt a single password + * @param string $password plain text password + * @return encrypted password + */ + private static function encryptPassword($password) { + $cipher = self::getCipher(); + $iv = \OCP\Util::generateRandomBytes(16); + $cipher->setIV($iv); + return base64_encode($iv . $cipher->encrypt($password)); + } + + /** + * Decrypts a single password + * @param string $encryptedPassword encrypted password + * @return plain text password + */ + private static function decryptPassword($encryptedPassword) { + $cipher = self::getCipher(); + $binaryPassword = base64_decode($encryptedPassword); + $iv = substr($binaryPassword, 0, 16); + $cipher->setIV($iv); + $binaryPassword = substr($binaryPassword, 16); + return $cipher->decrypt($binaryPassword); + } + + /** + * Returns the encryption cipher + */ + private static function getCipher() { + if (!class_exists('Crypt_AES', false)) { + include('Crypt/AES.php'); + } + $cipher = new Crypt_AES(CRYPT_AES_MODE_CBC); + $cipher->setKey(\OCP\Config::getSystemValue('passwordsalt')); + return $cipher; + } } diff --git a/apps/files_external/lib/smb.php b/apps/files_external/lib/smb.php index c5fba92ee68..f3f3b3ed7f3 100644 --- a/apps/files_external/lib/smb.php +++ b/apps/files_external/lib/smb.php @@ -37,7 +37,7 @@ class SMB extends \OC\Files\Storage\StreamWrapper{ $this->share = substr($this->share, 0, -1); } } else { - throw new \Exception(); + throw new \Exception('Invalid configuration'); } } diff --git a/apps/files_external/tests/mountconfig.php b/apps/files_external/tests/mountconfig.php index a22c7424c69..bf43bb31c38 100644 --- a/apps/files_external/tests/mountconfig.php +++ b/apps/files_external/tests/mountconfig.php @@ -40,9 +40,22 @@ class Test_Mount_Config extends \PHPUnit_Framework_TestCase { private $oldAllowedBackends; private $allBackends; + const TEST_USER1 = 'user1'; + const TEST_USER2 = 'user2'; + const TEST_GROUP1 = 'group1'; + const TEST_GROUP2 = 'group2'; + public function setUp() { - \OC_User::setUserId('test'); - $this->userHome = \OC_User::getHome('test'); + \OC_User::createUser(self::TEST_USER1, self::TEST_USER1); + \OC_User::createUser(self::TEST_USER2, self::TEST_USER2); + + \OC_Group::createGroup(self::TEST_GROUP1); + \OC_Group::addToGroup(self::TEST_USER1, self::TEST_GROUP1); + \OC_Group::createGroup(self::TEST_GROUP2); + \OC_Group::addToGroup(self::TEST_USER2, self::TEST_GROUP2); + + \OC_User::setUserId(self::TEST_USER1); + $this->userHome = \OC_User::getHome(self::TEST_USER1); mkdir($this->userHome); $this->dataDir = \OC_Config::getValue( @@ -67,9 +80,12 @@ class Test_Mount_Config extends \PHPUnit_Framework_TestCase { public function tearDown() { OC_Mount_Config::$skipTest = false; + \OC_User::deleteUser(self::TEST_USER2); + \OC_User::deleteUser(self::TEST_USER1); + \OC_Group::deleteGroup(self::TEST_GROUP1); + \OC_Group::deleteGroup(self::TEST_GROUP2); + @unlink($this->dataDir . '/mount.json'); - @unlink($this->userHome . '/mount.json'); - rmdir($this->userHome); OCP\Config::setAppValue( 'files_external', @@ -95,6 +111,14 @@ class Test_Mount_Config extends \PHPUnit_Framework_TestCase { } /** + * Write the user config, to simulate existing files + */ + private function writeUserConfig($config) { + $configFile = $this->userHome . '/mount.json'; + file_put_contents($configFile, json_encode($config)); + } + + /** * Test mount point validation */ public function testAddMountPointValidation() { @@ -113,7 +137,7 @@ class Test_Mount_Config extends \PHPUnit_Framework_TestCase { * Test adding a global mount point */ public function testAddGlobalMountPoint() { - $mountType = OC_Mount_Config::MOUNT_TYPE_GLOBAL; + $mountType = OC_Mount_Config::MOUNT_TYPE_USER; $applicable = 'all'; $isPersonal = false; @@ -135,7 +159,7 @@ class Test_Mount_Config extends \PHPUnit_Framework_TestCase { */ public function testAddMountPointSingleUser() { $mountType = OC_Mount_Config::MOUNT_TYPE_USER; - $applicable = 'test'; + $applicable = self::TEST_USER1; $isPersonal = true; $this->assertEquals(true, OC_Mount_Config::addMountPoint('/ext', '\OC\Files\Storage\SFTP', array(), $mountType, $applicable, $isPersonal)); @@ -144,10 +168,10 @@ class Test_Mount_Config extends \PHPUnit_Framework_TestCase { $this->assertEquals(1, count($config)); $this->assertTrue(isset($config[$mountType])); $this->assertTrue(isset($config[$mountType][$applicable])); - $this->assertTrue(isset($config[$mountType][$applicable]['/test/files/ext'])); + $this->assertTrue(isset($config[$mountType][$applicable]['/' . self::TEST_USER1 . '/files/ext'])); $this->assertEquals( '\OC\Files\Storage\SFTP', - $config[$mountType][$applicable]['/test/files/ext']['class'] + $config[$mountType][$applicable]['/' . self::TEST_USER1 . '/files/ext']['class'] ); } @@ -156,7 +180,7 @@ class Test_Mount_Config extends \PHPUnit_Framework_TestCase { */ public function testAddDisallowedBackendMountPointSingleUser() { $mountType = OC_Mount_Config::MOUNT_TYPE_USER; - $applicable = 'test'; + $applicable = self::TEST_USER1; $isPersonal = true; // local @@ -181,9 +205,274 @@ class Test_Mount_Config extends \PHPUnit_Framework_TestCase { public function testAddMountPointUnexistClass() { $storageClass = 'Unexist_Storage'; $mountType = OC_Mount_Config::MOUNT_TYPE_USER; - $applicable = 'test'; + $applicable = self::TEST_USER1; $isPersonal = false; $this->assertFalse(OC_Mount_Config::addMountPoint('/ext', $storageClass, array(), $mountType, $applicable, $isPersonal)); } + + /** + * Test reading and writing global config + */ + public function testReadWriteGlobalConfig() { + $mountType = OC_Mount_Config::MOUNT_TYPE_USER; + $applicable = 'all'; + $isPersonal = false; + $mountConfig = array( + 'host' => 'smbhost', + 'user' => 'smbuser', + 'password' => 'smbpassword', + 'share' => 'smbshare', + 'root' => 'smbroot' + ); + + // write config + $this->assertTrue( + OC_Mount_Config::addMountPoint( + '/ext', + '\OC\Files\Storage\SMB', + $mountConfig, + $mountType, + $applicable, + $isPersonal + ) + ); + + // re-read config + $config = OC_Mount_Config::getSystemMountPoints(); + $this->assertEquals(1, count($config)); + $this->assertTrue(isset($config['ext'])); + $this->assertEquals('\OC\Files\Storage\SMB', $config['ext']['class']); + $savedMountConfig = $config['ext']['configuration']; + $this->assertEquals($mountConfig, $savedMountConfig); + // key order needs to be preserved for the UI... + $this->assertEquals(array_keys($mountConfig), array_keys($savedMountConfig)); + } + + /** + * Test reading and writing config + */ + public function testReadWritePersonalConfig() { + $mountType = OC_Mount_Config::MOUNT_TYPE_USER; + $applicable = self::TEST_USER1; + $isPersonal = true; + $mountConfig = array( + 'host' => 'smbhost', + 'user' => 'smbuser', + 'password' => 'smbpassword', + 'share' => 'smbshare', + 'root' => 'smbroot' + ); + + // write config + $this->assertTrue( + OC_Mount_Config::addMountPoint( + '/ext', + '\OC\Files\Storage\SMB', + $mountConfig, + $mountType, + $applicable, + $isPersonal + ) + ); + + // re-read config + $config = OC_Mount_Config::getPersonalMountPoints(); + $this->assertEquals(1, count($config)); + $this->assertTrue(isset($config['ext'])); + $this->assertEquals('\OC\Files\Storage\SMB', $config['ext']['class']); + $savedMountConfig = $config['ext']['configuration']; + $this->assertEquals($mountConfig, $savedMountConfig); + // key order needs to be preserved for the UI... + $this->assertEquals(array_keys($mountConfig), array_keys($savedMountConfig)); + } + + /** + * Test password obfuscation + */ + public function testPasswordObfuscation() { + $mountType = OC_Mount_Config::MOUNT_TYPE_USER; + $applicable = self::TEST_USER1; + $isPersonal = true; + $mountConfig = array( + 'host' => 'smbhost', + 'user' => 'smbuser', + 'password' => 'smbpassword', + 'share' => 'smbshare', + 'root' => 'smbroot' + ); + + // write config + $this->assertTrue( + OC_Mount_Config::addMountPoint( + '/ext', + '\OC\Files\Storage\SMB', + $mountConfig, + $mountType, + $applicable, + $isPersonal + ) + ); + + // note: password re-reading is covered by testReadWritePersonalConfig + + // check that password inside the file is NOT in plain text + $config = $this->readUserConfig(); + $savedConfig = $config[$mountType][$applicable]['/' . self::TEST_USER1 . '/files/ext']['options']; + + // no more clear text password in file (kept because of key order) + $this->assertEquals('', $savedConfig['password']); + + // encrypted password is present + $this->assertNotEquals($mountConfig['password'], $savedConfig['password_encrypted']); + } + + /** + * Test read legacy passwords + */ + public function testReadLegacyPassword() { + $mountType = OC_Mount_Config::MOUNT_TYPE_USER; + $applicable = self::TEST_USER1; + $isPersonal = true; + $mountConfig = array( + 'host' => 'smbhost', + 'user' => 'smbuser', + 'password' => 'smbpassword', + 'share' => 'smbshare', + 'root' => 'smbroot' + ); + + // write config + $this->assertTrue( + OC_Mount_Config::addMountPoint( + '/ext', + '\OC\Files\Storage\SMB', + $mountConfig, + $mountType, + $applicable, + $isPersonal + ) + ); + + $config = $this->readUserConfig(); + // simulate non-encrypted password situation + $config[$mountType][$applicable]['/' . self::TEST_USER1 . '/files/ext']['options']['password'] = 'smbpasswd'; + + $this->writeUserConfig($config); + + // re-read config, password was read correctly + $config = OC_Mount_Config::getPersonalMountPoints(); + $savedMountConfig = $config['ext']['configuration']; + $this->assertEquals($mountConfig, $savedMountConfig); + } + + public function mountDataProvider() { + return array( + // Tests for visible mount points + // system mount point for all users + array( + false, + OC_Mount_Config::MOUNT_TYPE_USER, + 'all', + self::TEST_USER1, + true, + ), + // system mount point for a specific user + array( + false, + OC_Mount_Config::MOUNT_TYPE_USER, + self::TEST_USER1, + self::TEST_USER1, + true, + ), + // system mount point for a specific group + array( + false, + OC_Mount_Config::MOUNT_TYPE_GROUP, + self::TEST_GROUP1, + self::TEST_USER1, + true, + ), + // user mount point + array( + true, + OC_Mount_Config::MOUNT_TYPE_USER, + self::TEST_USER1, + self::TEST_USER1, + true, + ), + + // Tests for non-visible mount points + // system mount point for another user + array( + false, + OC_Mount_Config::MOUNT_TYPE_USER, + self::TEST_USER2, + self::TEST_USER1, + false, + ), + // system mount point for a specific group + array( + false, + OC_Mount_Config::MOUNT_TYPE_GROUP, + self::TEST_GROUP2, + self::TEST_USER1, + false, + ), + // user mount point + array( + true, + OC_Mount_Config::MOUNT_TYPE_USER, + self::TEST_USER1, + self::TEST_USER2, + false, + ), + ); + } + + /** + * Test mount points used at mount time, making sure + * the configuration is prepared properly. + * + * @dataProvider mountDataProvider + * @param bool $isPersonal true for personal mount point, false for system mount point + * @param string $mountType mount type + * @param string $applicable target user/group or "all" + * @param string $testUser user for which to retrieve the mount points + * @param bool $expectVisible whether to expect the mount point to be visible for $testUser + */ + public function testMount($isPersonal, $mountType, $applicable, $testUser, $expectVisible) { + $mountConfig = array( + 'host' => 'someost', + 'user' => 'someuser', + 'password' => 'somepassword', + 'root' => 'someroot' + ); + + // add mount point as "test" user + $this->assertTrue( + OC_Mount_Config::addMountPoint( + '/ext', + '\OC\Files\Storage\SMB', + $mountConfig, + $mountType, + $applicable, + $isPersonal + ) + ); + + // check mount points in the perspective of user $testUser + \OC_User::setUserId($testUser); + + $mountPoints = OC_Mount_Config::getAbsoluteMountPoints($testUser); + if ($expectVisible) { + $this->assertEquals(1, count($mountPoints)); + $this->assertTrue(isset($mountPoints['/' . self::TEST_USER1 . '/files/ext'])); + $this->assertEquals('\OC\Files\Storage\SMB', $mountPoints['/' . self::TEST_USER1 . '/files/ext']['class']); + $this->assertEquals($mountConfig, $mountPoints['/' . self::TEST_USER1 . '/files/ext']['options']); + } + else { + $this->assertEquals(0, count($mountPoints)); + } + } } |