user_saml icon indicating copy to clipboard operation
user_saml copied to clipboard

support for per-user encryption

Open summersab opened this issue 4 years ago • 14 comments

Implementing PR #498 from immerda. It's a feature I'd like to have, and he hasn't been active on GitHub since December. So, I'd like to submit the PR.

summersab avatar Jul 10 '21 20:07 summersab

Failing the DCO check. I edited the files via my browser because I have rather slow internet, and I didn't want to clone the whole repo via CLI. I'll go ahead and try to fix this.

EDIT: Fixed.

summersab avatar Jul 10 '21 20:07 summersab

I'm unsure what I need to do to fix the CI error. Any help?

summersab avatar Jul 11 '21 02:07 summersab

Regarding https://github.com/nextcloud/server/pull/27929#issuecomment-879181361 @summersab you mean the CI drone check? Not sure either, it says the "master" and "stable21" integration tests were cancelled, while the "stable22" integration tests was done and succeeded. Not sure how those work. I just confirmed to run another check (composer) which went well, but I don't think that is related to what drone does. Can you rebase onto current master, so I can see whether drone fails exact the same way?

MichaIng avatar Jul 13 '21 15:07 MichaIng

Failures seemed to be an unrelated timeout, I've retriggered the CI.

juliusknorr avatar Jul 13 '21 20:07 juliusknorr

I tried this patch and there are currently some issues:

The UserBackend is missing a implements IProvideUserSecretBackend. For that reason the secret is never requested by https://github.com/nextcloud/server/pull/27929/files#diff-b30a8c4cef5da5cede4d9038c16ede43e1746f85270e4249598fd305b0fa77deR182

The getCurrentUserSecret is missing the :string annotation from the interface.

getUserSecret returns null, which contradicts the type of the interface.

immerda avatar Nov 01 '21 23:11 immerda

For the record, here is my working version of the patch to UserBackend, which also requires the IProvideUserSecretBackend::getCurrentUserSecret() to be typed ?string instead of string:

--- a/apps/user_saml/lib/UserBackend.php
+++ b/apps/user_saml/lib/UserBackend.php
@@ -34,10 +34,11 @@ use OCP\IUserBackend;
 use OCP\IConfig;
 use OCP\IURLGenerator;
 use OCP\ISession;
+use OCP\Authentication\IProvideUserSecretBackend;
 use Symfony\Component\EventDispatcher\GenericEvent;
 use function base64_decode;
 
-class UserBackend implements IApacheBackend, UserInterface, IUserBackend {
+class UserBackend implements IApacheBackend, UserInterface, IUserBackend, IProvideUserSecretBackend {
        /** @var IConfig */
        private $config;
        /** @var IURLGenerator */
@@ -142,6 +143,12 @@ class UserBackend implements IApacheBackend, UserInterface, IUserBackend {
                                $qb->setValue($column, $qb->createNamedParameter($value));
                        }
                        $qb->execute();
+                       // If we use per-user encryption the keys must be initialized first
+                       $userSecret = $this->getUserSecret($uid, $attributes);
+                       if ($userSecret !== null) {
+                               // Emit a post login action to initialize the encryption module with the user secret provided by the idp.
+                               \OC_Hook::emit('OC_User', 'post_login', ['run' => true, 'uid' => $uid, 'password' => $userSecret, 'isTokenLogin' => false]);
+                       }
 
                        $this->initializeHomeDir($uid);
 
@@ -502,6 +509,16 @@ class UserBackend implements IApacheBackend, UserInterface, IUserBackend {
                return '';
        }
 
+   /**
+        * Optionally returns a stable per-user secret. This secret is for
+        * instance used to secure file encryption keys.
+        * @return string|null
+        * @since 23.0.0
+        */
+       public function getCurrentUserSecret(): ?string {
+               $samlData = $this->session->get('user_saml.samlUserData');
+               return $this->getUserSecret($this->getCurrentUserId(), $samlData);
+       }
 
        /**
         * Backend name to be shown in user management
@@ -600,6 +617,21 @@ class UserBackend implements IApacheBackend, UserInterface, IUserBackend {
                return $value;
        }
 
+       private function getUserSecret($uid, array $attributes) {
+               try {
+                       $userSecret = $this->getAttributeValue('saml-attribute-mapping-user_secret_mapping', $attributes);
+                       if ($userSecret === '') {
+                               $this->logger->debug('Got no user_secret from idp', ['app' => 'user_saml']);
+                       } else {
+                               $this->logger->debug('Got user_secret from idp', ['app' => 'user_saml']);
+                               return $userSecret;
+                       }
+               } catch (\InvalidArgumentException $e) {
+                       $this->logger->debug('No user_secret mapping configured', ['app' => 'user_saml']);
+               }
+               return null;
+       }
+
        public function updateAttributes($uid,
                                                                         array $attributes) {
                $user = $this->userManager->get($uid);

immerda avatar Nov 02 '21 11:11 immerda

Whether it's string or ?string needs to follow the interface vice versa and there was no final decision about yet: https://github.com/nextcloud/server/pull/27929/files#r695632608

Generally null passwords were made explicitly possible here: https://github.com/nextcloud/server/pull/29200 And/as of: https://github.com/nextcloud/server/pull/29187

But I lack the overview to reliably follow how everything is linked. As passwords intentionally were made nullable, looks like using ?string is the logical consequence. The other idea would be to conditionally implement the interface only if a password is actually provided. The same way on the current server-side interface code getCurrentUserSecret is only called if the interface is implemented by the backend, so that null means backend with no user secret support/enabled and a string means the backend supports it and then is also expected to provide it, hence a more explicit solution.

MichaIng avatar Nov 02 '21 12:11 MichaIng

@MichaIng thanks for bringing me up to speed on the discussion!

@summersab I had some time at hand, so here is my working version of this patch, that additionally fixes #547 https://gist.github.com/immerda/527cd5ef0c73cb0e5e4ed6e34c824324

immerda avatar Nov 02 '21 13:11 immerda

@summersab I had some time at hand, so here is my working version of this patch, that additionally fixes #547 https://gist.github.com/immerda/527cd5ef0c73cb0e5e4ed6e34c824324

@immerda Your patch is causing some errors after I apply it:

Doctrine\DBAL\Exception\NotNullConstraintViolationException
1364
An exception occurred while executing a query: SQLSTATE[HY000]: General error: 1364 Field 'name' doesn't have a default value
/var/www/nextcloud/3rdparty/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php
111

Trace
#0 /var/www/nextcloud/3rdparty/doctrine/dbal/src/Connection.php(1728): Doctrine\DBAL\Driver\API\MySQL\ExceptionConverter->convert()
#1 /var/www/nextcloud/3rdparty/doctrine/dbal/src/Connection.php(1667): Doctrine\DBAL\Connection->handleDriverException()
#2 /var/www/nextcloud/3rdparty/doctrine/dbal/src/Connection.php(1146): Doctrine\DBAL\Connection->convertExceptionDuringQuery()
#3 /var/www/nextcloud/lib/private/DB/Connection.php(262): Doctrine\DBAL\Connection->executeStatement()
#4 /var/www/nextcloud/3rdparty/doctrine/dbal/src/Query/QueryBuilder.php(213): OC\DB\Connection->executeStatement()
#5 /var/www/nextcloud/lib/private/DB/QueryBuilder/QueryBuilder.php(287): Doctrine\DBAL\Query\QueryBuilder->execute()
#6 /var/www/nextcloud/apps/user_saml/lib/UserBackend.php(198): OC\DB\QueryBuilder\QueryBuilder->execute()
#7 /var/www/nextcloud/apps/user_saml/lib/UserBackend.php(750): OCA\User_SAML\UserBackend->updateUserSecretHash()
#8 /var/www/nextcloud/apps/user_saml/lib/Controller/SAMLController.php(147): OCA\User_SAML\UserBackend->updateAttributes()
#9 /var/www/nextcloud/apps/user_saml/lib/Controller/SAMLController.php(335): OCA\User_SAML\Controller\SAMLController->autoprovisionIfPossible()
#10 /var/www/nextcloud/lib/private/AppFramework/Http/Dispatcher.php(217): OCA\User_SAML\Controller\SAMLController->assertionConsumerService()
#11 /var/www/nextcloud/lib/private/AppFramework/Http/Dispatcher.php(126): OC\AppFramework\Http\Dispatcher->executeController()
#12 /var/www/nextcloud/lib/private/AppFramework/App.php(156): OC\AppFramework\Http\Dispatcher->dispatch()
#13 /var/www/nextcloud/lib/private/Route/Router.php(301): OC\AppFramework\App::main()
#14 /var/www/nextcloud/lib/base.php(1000): OC\Route\Router->match()
#15 /var/www/nextcloud/index.php(36): OC::handleRequest()
#16 {main}

I'm pretty sure the rest of my code is clean and unmodified. Any clue?

summersab avatar Nov 12 '21 16:11 summersab

@summersab

Your patch is causing some errors after I apply it:

hah, good that somebody else tests it :) I used this unused table user_saml_auth_token to store the hashes, since it was already there... But that table seems to also have a field called name, for some reason and depending on the database backend, it complains if we don't set it to something.

so let's set the name to sso_secret_hash, it could be useful in the future if there are multiple kind of tokens...

I have update the patch. basically added lines 38, 67 and 73

once it is working you should see hashes being inserted into user_saml_auth_token

immerda avatar Nov 12 '21 16:11 immerda

Seems to be working, @immerda! I'll poke at it a little more over the weekend just to be sure.

summersab avatar Nov 12 '21 18:11 summersab

@summersab sorry, found another bug that is only triggered when the token exires. updated my gist. line 65 needs to be $qb->createNamedParameter($hash) instead of just $hash.

immerda avatar Nov 14 '21 21:11 immerda

You're a good man*, @immerda - thanks!

*Or woman - don't want to assume.

summersab avatar Nov 15 '21 13:11 summersab

https://github.com/nextcloud/server/pull/27929 has been merged, so work here can go on. When the new interface is used, a password must be set, the string is not nullable at server side.

MichaIng avatar Jul 18 '22 17:07 MichaIng