Merge pull request #7794 from owncloud/extstorage-obfuscatepasswords

Obfuscate passwords in ext storage config
This commit is contained in:
Lukas Reschke 2014-03-20 16:31:12 +01:00
commit 2c561c9c50
6 changed files with 495 additions and 86 deletions

View File

@ -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');

View File

@ -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(
@ -158,6 +166,96 @@ class OC_Mount_Config {
return($backends);
}
/**
* 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
@ -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;
}
}

View File

@ -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');
}
}

View File

@ -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',
@ -94,6 +110,14 @@ class Test_Mount_Config extends \PHPUnit_Framework_TestCase {
return json_decode(file_get_contents($configFile), true);
}
/**
* 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
*/
@ -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));
}
}
}

View File

@ -320,81 +320,11 @@ class Filesystem {
else {
self::mount('\OC\Files\Storage\Local', array('datadir' => $root), $user);
}
$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
if (is_file(\OC::$SERVERROOT . '/config/mount.php') or is_file($mount_file)) {
if (is_file($mount_file)) {
$mountConfig = json_decode(file_get_contents($mount_file), true);
} elseif (is_file(\OC::$SERVERROOT . '/config/mount.php')) {
$mountConfig = $parser->parsePHP(file_get_contents(\OC::$SERVERROOT . '/config/mount.php'));
}
if (isset($mountConfig['global'])) {
foreach ($mountConfig['global'] as $mountPoint => $options) {
self::mount($options['class'], $options['options'], $mountPoint);
}
}
if (isset($mountConfig['group'])) {
foreach ($mountConfig['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);
}
self::mount($options['class'], $options['options'], $mountPoint);
}
}
}
}
if (isset($mountConfig['user'])) {
foreach ($mountConfig['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);
}
self::mount($options['class'], $options['options'], $mountPoint);
}
}
}
}
}
// Load personal mount points
if (is_file($root . '/mount.php') or is_file($root . '/mount.json')) {
if (is_file($root . '/mount.json')) {
$mountConfig = json_decode(file_get_contents($root . '/mount.json'), true);
} elseif (is_file($root . '/mount.php')) {
$mountConfig = $parser->parsePHP(file_get_contents($root . '/mount.php'));
}
if (isset($mountConfig['user'][$user])) {
foreach ($mountConfig['user'][$user] as $mountPoint => $options) {
self::mount($options['class'], $options['options'], $mountPoint);
}
}
}
// Chance to mount for other storages
\OC_Hook::emit('OC_Filesystem', 'post_initMountPoints', array('user' => $user, 'user_dir' => $root));
}
/**
* 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 the default filesystem view
*

View File

@ -492,4 +492,13 @@ class Util {
public static function isValidFileName($file) {
return \OC_Util::isValidFileName($file);
}
/**
* @brief Generates a cryptographic secure pseudo-random string
* @param Int $length of the random string
* @return String
*/
public static function generateRandomBytes($length = 30) {
return \OC_Util::generateRandomBytes($length);
}
}