]> source.dussan.org Git - nextcloud-server.git/commitdiff
encrypt oauth2 client secrets 38708/head
authorJulien Veyssier <julien-nc@posteo.net>
Mon, 22 May 2023 13:39:56 +0000 (15:39 +0200)
committerJulien Veyssier <julien-nc@posteo.net>
Thu, 22 Jun 2023 12:09:05 +0000 (14:09 +0200)
Signed-off-by: Julien Veyssier <julien-nc@posteo.net>
12 files changed:
apps/oauth2/appinfo/info.xml
apps/oauth2/composer/composer/LICENSE
apps/oauth2/composer/composer/autoload_classmap.php
apps/oauth2/composer/composer/autoload_static.php
apps/oauth2/lib/Controller/OauthApiController.php
apps/oauth2/lib/Controller/SettingsController.php
apps/oauth2/lib/Migration/Version011601Date20230522143227.php [new file with mode: 0644]
apps/oauth2/lib/Settings/Admin.php
apps/oauth2/tests/Controller/OauthApiControllerTest.php
apps/oauth2/tests/Controller/SettingsControllerTest.php
apps/oauth2/tests/Settings/AdminTest.php
lib/composer/composer/LICENSE

index c9a2d46045004f16a76f8f7eecc08857f8be070b..a9247099988a0ff7530f3e92f41be1c8d20672b9 100644 (file)
@@ -5,7 +5,7 @@
        <name>OAuth 2.0</name>
        <summary>Allows OAuth2 compatible authentication from other web applications.</summary>
        <description>The OAuth2 app allows administrators to configure the built-in authentication workflow to also allow OAuth2 compatible authentication from other web applications.</description>
-       <version>1.14.0</version>
+       <version>1.14.1</version>
        <licence>agpl</licence>
        <author>Lukas Reschke</author>
        <namespace>OAuth2</namespace>
index f27399a042d95c4708af3a8c74d35d338763cf8f..62ecfd8d0046b60517ea7370300f52744f1ab85d 100644 (file)
@@ -1,4 +1,3 @@
-
 Copyright (c) Nils Adermann, Jordi Boggiano
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -18,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
-
index d760d7cd57929af23b27d3e45c7d14da8461a843..09cacb20335f96161c5cabd45cfc4726710a2856 100644 (file)
@@ -19,5 +19,6 @@ return array(
     'OCA\\OAuth2\\Migration\\SetTokenExpiration' => $baseDir . '/../lib/Migration/SetTokenExpiration.php',
     'OCA\\OAuth2\\Migration\\Version010401Date20181207190718' => $baseDir . '/../lib/Migration/Version010401Date20181207190718.php',
     'OCA\\OAuth2\\Migration\\Version010402Date20190107124745' => $baseDir . '/../lib/Migration/Version010402Date20190107124745.php',
+    'OCA\\OAuth2\\Migration\\Version011601Date20230522143227' => $baseDir . '/../lib/Migration/Version011601Date20230522143227.php',
     'OCA\\OAuth2\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php',
 );
index c8f3a388c786fa66c8dca73af66ec00e05d352f5..1442093e32fb93caf92f48c8a1001c79ac032281 100644 (file)
@@ -34,6 +34,7 @@ class ComposerStaticInitOAuth2
         'OCA\\OAuth2\\Migration\\SetTokenExpiration' => __DIR__ . '/..' . '/../lib/Migration/SetTokenExpiration.php',
         'OCA\\OAuth2\\Migration\\Version010401Date20181207190718' => __DIR__ . '/..' . '/../lib/Migration/Version010401Date20181207190718.php',
         'OCA\\OAuth2\\Migration\\Version010402Date20190107124745' => __DIR__ . '/..' . '/../lib/Migration/Version010402Date20190107124745.php',
+        'OCA\\OAuth2\\Migration\\Version011601Date20230522143227' => __DIR__ . '/..' . '/../lib/Migration/Version011601Date20230522143227.php',
         'OCA\\OAuth2\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php',
     );
 
index 910fdc994323061b8de6676855d2c52e40d475a1..0dcc2a2cf712a000e1516ee543faff8cef0c82dd 100644 (file)
@@ -42,6 +42,7 @@ use OCP\AppFramework\Utility\ITimeFactory;
 use OCP\IRequest;
 use OCP\Security\ICrypto;
 use OCP\Security\ISecureRandom;
+use Psr\Log\LoggerInterface;
 
 class OauthApiController extends Controller {
        /** @var AccessTokenMapper */
@@ -58,6 +59,8 @@ class OauthApiController extends Controller {
        private $time;
        /** @var Throttler */
        private $throttler;
+       /** @var LoggerInterface */
+       private $logger;
 
        public function __construct(string $appName,
                                                                IRequest $request,
@@ -67,6 +70,7 @@ class OauthApiController extends Controller {
                                                                TokenProvider $tokenProvider,
                                                                ISecureRandom $secureRandom,
                                                                ITimeFactory $time,
+                                                               LoggerInterface $logger,
                                                                Throttler $throttler) {
                parent::__construct($appName, $request);
                $this->crypto = $crypto;
@@ -76,6 +80,7 @@ class OauthApiController extends Controller {
                $this->secureRandom = $secureRandom;
                $this->time = $time;
                $this->throttler = $throttler;
+               $this->logger = $logger;
        }
 
        /**
@@ -124,8 +129,16 @@ class OauthApiController extends Controller {
                        $client_secret = $this->request->server['PHP_AUTH_PW'];
                }
 
+               try {
+                       $storedClientSecret = $this->crypto->decrypt($client->getSecret());
+               } catch (\Exception $e) {
+                       $this->logger->error('OAuth client secret decryption error', ['exception' => $e]);
+                       return new JSONResponse([
+                               'error' => 'invalid_client',
+                       ], Http::STATUS_BAD_REQUEST);
+               }
                // The client id and secret must match. Else we don't provide an access token!
-               if ($client->getClientIdentifier() !== $client_id || $client->getSecret() !== $client_secret) {
+               if ($client->getClientIdentifier() !== $client_id || $storedClientSecret !== $client_secret) {
                        return new JSONResponse([
                                'error' => 'invalid_client',
                        ], Http::STATUS_BAD_REQUEST);
index 04fc75a8f4a3503a71c747311fe1a61877026583..6c57a4b9a2b3a3c736bb7a39c50ca481ce9103bb 100644 (file)
@@ -42,6 +42,7 @@ use OCP\IRequest;
 use OCP\IUser;
 use OCP\IUserManager;
 use OCP\Security\ISecureRandom;
+use OCP\Security\ICrypto;
 
 class SettingsController extends Controller {
        /** @var ClientMapper */
@@ -58,6 +59,9 @@ class SettingsController extends Controller {
         * @var IUserManager
         */
        private $userManager;
+       /** @var ICrypto */
+       private $crypto;
+
        public const validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
 
        public function __construct(string $appName,
@@ -67,7 +71,8 @@ class SettingsController extends Controller {
                                                                AccessTokenMapper $accessTokenMapper,
                                                                IL10N $l,
                                                                IAuthTokenProvider $tokenProvider,
-                                                               IUserManager $userManager
+                                                               IUserManager $userManager,
+                                                               ICrypto $crypto
        ) {
                parent::__construct($appName, $request);
                $this->secureRandom = $secureRandom;
@@ -76,6 +81,7 @@ class SettingsController extends Controller {
                $this->l = $l;
                $this->tokenProvider = $tokenProvider;
                $this->userManager = $userManager;
+               $this->crypto = $crypto;
        }
 
        public function addClient(string $name,
@@ -87,7 +93,9 @@ class SettingsController extends Controller {
                $client = new Client();
                $client->setName($name);
                $client->setRedirectUri($redirectUri);
-               $client->setSecret($this->secureRandom->generate(64, self::validChars));
+               $secret = $this->secureRandom->generate(64, self::validChars);
+               $encryptedSecret = $this->crypto->encrypt($secret);
+               $client->setSecret($encryptedSecret);
                $client->setClientIdentifier($this->secureRandom->generate(64, self::validChars));
                $client = $this->clientMapper->insert($client);
 
@@ -96,7 +104,7 @@ class SettingsController extends Controller {
                        'name' => $client->getName(),
                        'redirectUri' => $client->getRedirectUri(),
                        'clientId' => $client->getClientIdentifier(),
-                       'clientSecret' => $client->getSecret(),
+                       'clientSecret' => $secret,
                ];
 
                return new JSONResponse($result);
diff --git a/apps/oauth2/lib/Migration/Version011601Date20230522143227.php b/apps/oauth2/lib/Migration/Version011601Date20230522143227.php
new file mode 100644 (file)
index 0000000..43e3a2e
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright 2023, Julien Veyssier <julien-nc@posteo.net>
+ *
+ * @author Julien Veyssier <julien-nc@posteo.net>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCA\OAuth2\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+use OCP\Security\ICrypto;
+
+class Version011601Date20230522143227 extends SimpleMigrationStep {
+
+       public function __construct(
+               private IDBConnection $connection,
+               private ICrypto $crypto,
+       ) {
+       }
+
+       public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
+               /** @var ISchemaWrapper $schema */
+               $schema = $schemaClosure();
+
+               if ($schema->hasTable('oauth2_clients')) {
+                       $table = $schema->getTable('oauth2_clients');
+                       if ($table->hasColumn('secret')) {
+                               $column = $table->getColumn('secret');
+                               $column->setLength(512);
+                               return $schema;
+                       }
+               }
+
+               return null;
+       }
+
+       public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) {
+               $qbUpdate = $this->connection->getQueryBuilder();
+               $qbUpdate->update('oauth2_clients')
+                       ->set('secret', $qbUpdate->createParameter('updateSecret'))
+                       ->where(
+                               $qbUpdate->expr()->eq('id', $qbUpdate->createParameter('updateId'))
+                       );
+
+               $qbSelect = $this->connection->getQueryBuilder();
+               $qbSelect->select('id', 'secret')
+                       ->from('oauth2_clients');
+               $req = $qbSelect->executeQuery();
+               while ($row = $req->fetch()) {
+                       $id = $row['id'];
+                       $secret = $row['secret'];
+                       $encryptedSecret = $this->crypto->encrypt($secret);
+                       $qbUpdate->setParameter('updateSecret', $encryptedSecret, IQueryBuilder::PARAM_STR);
+                       $qbUpdate->setParameter('updateId', $id, IQueryBuilder::PARAM_INT);
+                       $qbUpdate->executeStatement();
+               }
+               $req->closeCursor();
+       }
+}
index aa2bd6db0121f5bdd58fe996697394e91db23cee..eb809f27d6f72df6630d3d31724a48a472a893d6 100644 (file)
@@ -29,22 +29,30 @@ namespace OCA\OAuth2\Settings;
 use OCA\OAuth2\Db\ClientMapper;
 use OCP\AppFramework\Http\TemplateResponse;
 use OCP\AppFramework\Services\IInitialState;
+use OCP\Security\ICrypto;
 use OCP\Settings\ISettings;
 use OCP\IURLGenerator;
+use Psr\Log\LoggerInterface;
 
 class Admin implements ISettings {
        private IInitialState $initialState;
        private ClientMapper $clientMapper;
        private IURLGenerator $urlGenerator;
+       private ICrypto $crypto;
+       private LoggerInterface $logger;
 
        public function __construct(
                IInitialState $initialState,
                ClientMapper $clientMapper,
-               IURLGenerator $urlGenerator
+               IURLGenerator $urlGenerator,
+               ICrypto $crypto,
+               LoggerInterface $logger
        ) {
                $this->initialState = $initialState;
                $this->clientMapper = $clientMapper;
                $this->urlGenerator = $urlGenerator;
+               $this->crypto = $crypto;
+               $this->logger = $logger;
        }
 
        public function getForm(): TemplateResponse {
@@ -52,13 +60,18 @@ class Admin implements ISettings {
                $result = [];
 
                foreach ($clients as $client) {
-                       $result[] = [
-                               'id' => $client->getId(),
-                               'name' => $client->getName(),
-                               'redirectUri' => $client->getRedirectUri(),
-                               'clientId' => $client->getClientIdentifier(),
-                               'clientSecret' => $client->getSecret(),
-                       ];
+                       try {
+                               $secret = $this->crypto->decrypt($client->getSecret());
+                               $result[] = [
+                                       'id' => $client->getId(),
+                                       'name' => $client->getName(),
+                                       'redirectUri' => $client->getRedirectUri(),
+                                       'clientId' => $client->getClientIdentifier(),
+                                       'clientSecret' => $secret,
+                               ];
+                       } catch (\Exception $e) {
+                               $this->logger->error('[Settings] OAuth client secret decryption error', ['exception' => $e]);
+                       }
                }
                $this->initialState->provideInitialState('clients', $result);
                $this->initialState->provideInitialState('oauth2-doc-link', $this->urlGenerator->linkToDocs('admin-oauth2'));
index 8977f6a2b66259b582ab82d678b01c7b6cadb611..eb9311dbbc7552bdb55f6c5cebfb3c58bdb07481 100644 (file)
@@ -43,6 +43,7 @@ use OCP\AppFramework\Utility\ITimeFactory;
 use OCP\IRequest;
 use OCP\Security\ICrypto;
 use OCP\Security\ISecureRandom;
+use Psr\Log\LoggerInterface;
 use Test\TestCase;
 
 /* We have to use this to add a property to the mocked request and avoid warnings about dynamic properties on PHP>=8.2 */
@@ -67,6 +68,8 @@ class OauthApiControllerTest extends TestCase {
        private $time;
        /** @var Throttler|\PHPUnit\Framework\MockObject\MockObject */
        private $throttler;
+       /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
+       private $logger;
        /** @var OauthApiController */
        private $oauthApiController;
 
@@ -81,6 +84,7 @@ class OauthApiControllerTest extends TestCase {
                $this->secureRandom = $this->createMock(ISecureRandom::class);
                $this->time = $this->createMock(ITimeFactory::class);
                $this->throttler = $this->createMock(Throttler::class);
+               $this->logger = $this->createMock(LoggerInterface::class);
 
                $this->oauthApiController = new OauthApiController(
                        'oauth2',
@@ -91,6 +95,7 @@ class OauthApiControllerTest extends TestCase {
                        $this->tokenProvider,
                        $this->secureRandom,
                        $this->time,
+                       $this->logger,
                        $this->throttler
                );
        }
@@ -198,16 +203,21 @@ class OauthApiControllerTest extends TestCase {
 
                $client = new Client();
                $client->setClientIdentifier('clientId');
-               $client->setSecret('clientSecret');
+               $client->setSecret('encryptedClientSecret');
                $this->clientMapper->method('getByUid')
                        ->with(42)
                        ->willReturn($client);
 
-               $this->crypto->method('decrypt')
-                       ->with(
-                               'encryptedToken',
-                               'validrefresh'
-                       )->willReturn('decryptedToken');
+               $this->crypto
+                       ->method('decrypt')
+                       ->with($this->callback(function (string $text) {
+                               return $text === 'encryptedClientSecret' || $text === 'encryptedToken';
+                       }))
+                       ->willReturnCallback(function (string $text) {
+                               return $text === 'encryptedClientSecret'
+                                       ? 'clientSecret'
+                                       : ($text === 'encryptedToken' ? 'decryptedToken' : '');
+                       });
 
                $this->tokenProvider->method('getTokenById')
                        ->with(1337)
@@ -232,16 +242,21 @@ class OauthApiControllerTest extends TestCase {
 
                $client = new Client();
                $client->setClientIdentifier('clientId');
-               $client->setSecret('clientSecret');
+               $client->setSecret('encryptedClientSecret');
                $this->clientMapper->method('getByUid')
                        ->with(42)
                        ->willReturn($client);
 
-               $this->crypto->method('decrypt')
-                       ->with(
-                               'encryptedToken',
-                               'validrefresh'
-                       )->willReturn('decryptedToken');
+               $this->crypto
+                       ->method('decrypt')
+                       ->with($this->callback(function (string $text) {
+                               return $text === 'encryptedClientSecret' || $text === 'encryptedToken';
+                       }))
+                       ->willReturnCallback(function (string $text) {
+                               return $text === 'encryptedClientSecret'
+                                       ? 'clientSecret'
+                                       : ($text === 'encryptedToken' ? 'decryptedToken' : '');
+                       });
 
                $appToken = new PublicKeyToken();
                $appToken->setUid('userId');
@@ -324,16 +339,21 @@ class OauthApiControllerTest extends TestCase {
 
                $client = new Client();
                $client->setClientIdentifier('clientId');
-               $client->setSecret('clientSecret');
+               $client->setSecret('encryptedClientSecret');
                $this->clientMapper->method('getByUid')
                        ->with(42)
                        ->willReturn($client);
 
-               $this->crypto->method('decrypt')
-                       ->with(
-                               'encryptedToken',
-                               'validrefresh'
-                       )->willReturn('decryptedToken');
+               $this->crypto
+                       ->method('decrypt')
+                       ->with($this->callback(function (string $text) {
+                               return $text === 'encryptedClientSecret' || $text === 'encryptedToken';
+                       }))
+                       ->willReturnCallback(function (string $text) {
+                               return $text === 'encryptedClientSecret'
+                                       ? 'clientSecret'
+                                       : ($text === 'encryptedToken' ? 'decryptedToken' : '');
+                       });
 
                $appToken = new PublicKeyToken();
                $appToken->setUid('userId');
@@ -419,16 +439,21 @@ class OauthApiControllerTest extends TestCase {
 
                $client = new Client();
                $client->setClientIdentifier('clientId');
-               $client->setSecret('clientSecret');
+               $client->setSecret('encryptedClientSecret');
                $this->clientMapper->method('getByUid')
                        ->with(42)
                        ->willReturn($client);
 
-               $this->crypto->method('decrypt')
-                       ->with(
-                               'encryptedToken',
-                               'validrefresh'
-                       )->willReturn('decryptedToken');
+               $this->crypto
+                       ->method('decrypt')
+                       ->with($this->callback(function (string $text) {
+                               return $text === 'encryptedClientSecret' || $text === 'encryptedToken';
+                       }))
+                       ->willReturnCallback(function (string $text) {
+                               return $text === 'encryptedClientSecret'
+                                       ? 'clientSecret'
+                                       : ($text === 'encryptedToken' ? 'decryptedToken' : '');
+                       });
 
                $appToken = new PublicKeyToken();
                $appToken->setUid('userId');
index fe26fb4607adc4f15dab61db440e5df1b12d2de3..739c9aaa0e57fc72ad75239d0787edf32de699c1 100644 (file)
@@ -38,6 +38,7 @@ use OCP\IL10N;
 use OCP\IRequest;
 use OCP\IUser;
 use OCP\IUserManager;
+use OCP\Security\ICrypto;
 use OCP\Security\ISecureRandom;
 use Test\TestCase;
 
@@ -61,6 +62,8 @@ class SettingsControllerTest extends TestCase {
        private $settingsController;
        /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
        private $l;
+       /** @var ICrypto|\PHPUnit\Framework\MockObject\MockObject */
+       private $crypto;
 
        protected function setUp(): void {
                parent::setUp();
@@ -74,6 +77,8 @@ class SettingsControllerTest extends TestCase {
                $this->l = $this->createMock(IL10N::class);
                $this->l->method('t')
                        ->willReturnArgument(0);
+               $this->crypto = $this->createMock(ICrypto::class);
+
                $this->settingsController = new SettingsController(
                        'oauth2',
                        $this->request,
@@ -82,7 +87,8 @@ class SettingsControllerTest extends TestCase {
                        $this->accessTokenMapper,
                        $this->l,
                        $this->authTokenProvider,
-                       $this->userManager
+                       $this->userManager,
+                       $this->crypto
                );
 
        }
@@ -96,6 +102,11 @@ class SettingsControllerTest extends TestCase {
                                'MySecret',
                                'MyClientIdentifier');
 
+               $this->crypto
+                       ->expects($this->once())
+                       ->method('encrypt')
+                       ->willReturn('MyEncryptedSecret');
+
                $client = new Client();
                $client->setName('My Client Name');
                $client->setRedirectUri('https://example.com/');
@@ -108,7 +119,7 @@ class SettingsControllerTest extends TestCase {
                        ->with($this->callback(function (Client $c) {
                                return $c->getName() === 'My Client Name' &&
                                        $c->getRedirectUri() === 'https://example.com/' &&
-                                       $c->getSecret() === 'MySecret' &&
+                                       $c->getSecret() === 'MyEncryptedSecret' &&
                                        $c->getClientIdentifier() === 'MyClientIdentifier';
                        }))->willReturnCallback(function (Client $c) {
                                $c->setId(42);
@@ -178,7 +189,8 @@ class SettingsControllerTest extends TestCase {
                        $this->accessTokenMapper,
                        $this->l,
                        $tokenProviderMock,
-                       $userManager
+                       $userManager,
+                       $this->crypto
                );
 
                $result = $settingsController->deleteClient(123);
index fc5ebbb83651d0ba6da38c7eadf722cb11e9a829..afbed91b078982ef0263516a7018e1e783264d0e 100644 (file)
@@ -28,7 +28,9 @@ use OCA\OAuth2\Settings\Admin;
 use OCP\AppFramework\Http\TemplateResponse;
 use OCP\AppFramework\Services\IInitialState;
 use OCP\IURLGenerator;
+use OCP\Security\ICrypto;
 use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
 use Test\TestCase;
 
 class AdminTest extends TestCase {
@@ -36,7 +38,7 @@ class AdminTest extends TestCase {
        /** @var Admin|MockObject */
        private $admin;
 
-       /** @var IInitialStateService|MockObject */
+       /** @var IInitialState|MockObject */
        private $initialState;
 
        /** @var ClientMapper|MockObject */
@@ -48,7 +50,13 @@ class AdminTest extends TestCase {
                $this->initialState = $this->createMock(IInitialState::class);
                $this->clientMapper = $this->createMock(ClientMapper::class);
 
-               $this->admin = new Admin($this->initialState, $this->clientMapper, $this->createMock(IURLGenerator::class));
+               $this->admin = new Admin(
+                       $this->initialState,
+                       $this->clientMapper,
+                       $this->createMock(IURLGenerator::class),
+                       $this->createMock(ICrypto::class),
+                       $this->createMock(LoggerInterface::class)
+               );
        }
 
        public function testGetForm() {
index f27399a042d95c4708af3a8c74d35d338763cf8f..62ecfd8d0046b60517ea7370300f52744f1ab85d 100644 (file)
@@ -1,4 +1,3 @@
-
 Copyright (c) Nils Adermann, Jordi Boggiano
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -18,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
-