diff --git a/app/Http/Controllers/Base/APIController.php b/app/Http/Controllers/Base/APIController.php index c73661777..cedba8963 100644 --- a/app/Http/Controllers/Base/APIController.php +++ b/app/Http/Controllers/Base/APIController.php @@ -49,8 +49,6 @@ class APIController extends Controller * * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function index(Request $request) { diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 58772699f..9d8478e9d 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -14,19 +14,19 @@ use Pterodactyl\Http\Middleware\AdminAuthenticate; use Illuminate\Routing\Middleware\ThrottleRequests; use Pterodactyl\Http\Middleware\LanguageMiddleware; use Illuminate\Foundation\Http\Kernel as HttpKernel; -use Pterodactyl\Http\Middleware\API\AuthenticateKey; use Illuminate\Routing\Middleware\SubstituteBindings; use Pterodactyl\Http\Middleware\AccessingValidServer; -use Pterodactyl\Http\Middleware\API\SetSessionDriver; use Illuminate\View\Middleware\ShareErrorsFromSession; use Pterodactyl\Http\Middleware\RedirectIfAuthenticated; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; -use Pterodactyl\Http\Middleware\API\AuthenticateIPAccess; -use Pterodactyl\Http\Middleware\Daemon\DaemonAuthenticate; +use Pterodactyl\Http\Middleware\Api\Admin\AuthenticateKey; use Illuminate\Foundation\Http\Middleware\ValidatePostSize; +use Pterodactyl\Http\Middleware\Api\Admin\SetSessionDriver; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser; +use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate; use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer; +use Pterodactyl\Http\Middleware\Api\Admin\AuthenticateIPAccess; use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication; use Pterodactyl\Http\Middleware\Server\DatabaseBelongsToServer; use Pterodactyl\Http\Middleware\Server\ScheduleBelongsToServer; diff --git a/app/Http/Middleware/API/AuthenticateIPAccess.php b/app/Http/Middleware/Api/Admin/AuthenticateIPAccess.php similarity index 95% rename from app/Http/Middleware/API/AuthenticateIPAccess.php rename to app/Http/Middleware/Api/Admin/AuthenticateIPAccess.php index aa0af7e2e..d3ff6b3c6 100644 --- a/app/Http/Middleware/API/AuthenticateIPAccess.php +++ b/app/Http/Middleware/Api/Admin/AuthenticateIPAccess.php @@ -1,6 +1,6 @@ auth = $auth; + $this->encrypter = $encrypter; $this->repository = $repository; } /** * Handle an API request by verifying that the provided API key - * is in a valid format, and the route being accessed is allowed - * for the given key. + * is in a valid format and exists in the database. * * @param \Illuminate\Http\Request $request * @param \Closure $next @@ -54,12 +60,20 @@ class AuthenticateKey throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); } + $raw = $request->bearerToken(); + $identifier = substr($raw, 0, APIKey::IDENTIFIER_LENGTH); + $token = substr($raw, APIKey::IDENTIFIER_LENGTH); + try { - $model = $this->repository->findFirstWhere([['token', '=', $request->bearerToken()]]); + $model = $this->repository->findFirstWhere([['identifier', '=', $identifier]]); } catch (RecordNotFoundException $exception) { throw new AccessDeniedHttpException; } + if (! hash_equals($this->encrypter->decrypt($model->token), $token)) { + throw new AccessDeniedHttpException; + } + $this->auth->guard()->loginUsingId($model->user_id); $request->attributes->set('api_key', $model); diff --git a/app/Http/Middleware/API/SetSessionDriver.php b/app/Http/Middleware/Api/Admin/SetSessionDriver.php similarity index 96% rename from app/Http/Middleware/API/SetSessionDriver.php rename to app/Http/Middleware/Api/Admin/SetSessionDriver.php index 9cc5d60e3..d42fc7408 100644 --- a/app/Http/Middleware/API/SetSessionDriver.php +++ b/app/Http/Middleware/Api/Admin/SetSessionDriver.php @@ -1,6 +1,6 @@ . - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * 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. - */ -namespace Pterodactyl\Http\Middleware\Daemon; +namespace Pterodactyl\Http\Middleware\Api\Daemon; use Closure; use Illuminate\Http\Request; diff --git a/app/Models/APIKey.php b/app/Models/APIKey.php index f1cf29e33..8edab2bdd 100644 --- a/app/Models/APIKey.php +++ b/app/Models/APIKey.php @@ -6,6 +6,7 @@ use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Illuminate\Contracts\Encryption\Encrypter; use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; @@ -13,6 +14,15 @@ class APIKey extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; + /** + * The length of API key identifiers. + */ + const IDENTIFIER_LENGTH = 16; + + /** + * The length of the actual API key that is encrypted and stored + * in the database. + */ const KEY_LENGTH = 32; /** @@ -47,18 +57,27 @@ class APIKey extends Model implements CleansAttributes, ValidableContract * @var array */ protected $fillable = [ + 'identifier', 'token', 'allowed_ips', 'memo', - 'expires_at', ]; + /** + * Fields that should not be included when calling toArray() or toJson() + * on this model. + * + * @var array + */ + protected $hidden = ['token']; + /** * Rules defining what fields must be passed when making a model. * * @var array */ protected static $applicationRules = [ + 'identifier' => 'required', 'memo' => 'required', 'user_id' => 'required', 'token' => 'required', @@ -71,10 +90,11 @@ class APIKey extends Model implements CleansAttributes, ValidableContract */ protected static $dataIntegrityRules = [ 'user_id' => 'exists:users,id', - 'token' => 'string|size:32', + 'identifier' => 'string|size:16|unique:api_keys,identifier', + 'token' => 'string', 'memo' => 'nullable|string|max:500', 'allowed_ips' => 'nullable|json', - 'expires_at' => 'nullable|datetime', + 'last_used_at' => 'nullable|date', 'r_' . AdminAcl::RESOURCE_USERS => 'integer|min:0|max:3', 'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'integer|min:0|max:3', 'r_' . AdminAcl::RESOURCE_DATABASES => 'integer|min:0|max:3', @@ -92,9 +112,19 @@ class APIKey extends Model implements CleansAttributes, ValidableContract protected $dates = [ self::CREATED_AT, self::UPDATED_AT, - 'expires_at', + 'last_used_at', ]; + /** + * Return a decrypted version of the token. + * + * @return string + */ + public function getDecryptedTokenAttribute() + { + return app()->make(Encrypter::class)->decrypt($this->token); + } + /** * Gets the permissions associated with a key. * diff --git a/app/Providers/BladeServiceProvider.php b/app/Providers/BladeServiceProvider.php new file mode 100644 index 000000000..97b0df48e --- /dev/null +++ b/app/Providers/BladeServiceProvider.php @@ -0,0 +1,19 @@ +app->make('blade.compiler') + ->directive('datetimeHuman', function ($expression) { + return "setTimezone(config('app.timezone'))->toDateTimeString(); ?>"; + }); + } +} diff --git a/app/Services/Api/KeyCreationService.php b/app/Services/Api/KeyCreationService.php index 891a32438..c740c6492 100644 --- a/app/Services/Api/KeyCreationService.php +++ b/app/Services/Api/KeyCreationService.php @@ -4,6 +4,7 @@ namespace Pterodactyl\Services\Api; use Pterodactyl\Models\APIKey; use Illuminate\Database\ConnectionInterface; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; class KeyCreationService @@ -14,9 +15,9 @@ class KeyCreationService private $connection; /** - * @var \Pterodactyl\Services\Api\PermissionService + * @var \Illuminate\Contracts\Encryption\Encrypter */ - private $permissionService; + private $encrypter; /** * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface @@ -28,67 +29,36 @@ class KeyCreationService * * @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Pterodactyl\Services\Api\PermissionService $permissionService + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter */ public function __construct( ApiKeyRepositoryInterface $repository, ConnectionInterface $connection, - PermissionService $permissionService + Encrypter $encrypter ) { $this->repository = $repository; $this->connection = $connection; - $this->permissionService = $permissionService; + $this->encrypter = $encrypter; } /** - * Create a new API Key on the system with the given permissions. + * Create a new API key for the Panel using the permissions passed in the data request. + * This will automatically generate an identifer and an encrypted token that are + * stored in the database. * * @param array $data - * @param array $permissions - * @param array $administrative * @return \Pterodactyl\Models\APIKey * - * @throws \Exception * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function handle(array $data, array $permissions, array $administrative = []): APIKey + public function handle(array $data): APIKey { - $token = str_random(APIKey::KEY_LENGTH); - $data = array_merge($data, ['token' => $token]); + $data = array_merge($data, [ + 'identifer' => str_random(APIKey::IDENTIFIER_LENGTH), + 'token' => $this->encrypter->encrypt(str_random(APIKey::KEY_LENGTH)), + ]); - $this->connection->beginTransaction(); $instance = $this->repository->create($data, true, true); - $nodes = $this->permissionService->getPermissions(); - - foreach ($permissions as $permission) { - @list($block, $search) = explode('-', $permission, 2); - - if ( - (empty($block) || empty($search)) || - ! array_key_exists($block, $nodes['_user']) || - ! in_array($search, $nodes['_user'][$block]) - ) { - continue; - } - - $this->permissionService->create($instance->id, sprintf('user.%s', $permission)); - } - - foreach ($administrative as $permission) { - @list($block, $search) = explode('-', $permission, 2); - - if ( - (empty($block) || empty($search)) || - ! array_key_exists($block, $nodes) || - ! in_array($search, $nodes[$block]) - ) { - continue; - } - - $this->permissionService->create($instance->id, $permission); - } - - $this->connection->commit(); return $instance; } diff --git a/config/app.php b/config/app.php index 4a8de1e36..9a346918c 100644 --- a/config/app.php +++ b/config/app.php @@ -175,6 +175,7 @@ return [ */ Pterodactyl\Providers\AppServiceProvider::class, Pterodactyl\Providers\AuthServiceProvider::class, + Pterodactyl\Providers\BladeServiceProvider::class, Pterodactyl\Providers\EventServiceProvider::class, Pterodactyl\Providers\HashidsServiceProvider::class, Pterodactyl\Providers\RouteServiceProvider::class, diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 6e585d4d6..810f81bdc 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -227,7 +227,8 @@ $factory->define(Pterodactyl\Models\APIKey::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'user_id' => $faker->randomNumber(), - 'token' => str_random(Pterodactyl\Models\APIKey::KEY_LENGTH), + 'identifier' => str_random(Pterodactyl\Models\APIKey::IDENTIFIER_LENGTH), + 'token' => 'encrypted_string', 'memo' => 'Test Function Key', 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), diff --git a/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php b/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php new file mode 100644 index 000000000..01e4dcb76 --- /dev/null +++ b/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php @@ -0,0 +1,62 @@ +char('identifier', 16)->unique()->after('user_id'); + $table->dropUnique(['token']); + }); + + Schema::table('api_keys', function (Blueprint $table) { + $table->text('token')->change(); + }); + + DB::transaction(function () { + foreach (DB::table('api_keys')->cursor() as $key) { + DB::table('api_keys')->where('id', $key->id)->update([ + 'identifier' => str_random(16), + 'token' => Crypt::encrypt($key->token), + ]); + } + }); + } + + /** + * Reverse the migrations. + * + * @return void + * @throws \Exception + * @throws \Throwable + */ + public function down() + { + /** @var \Pterodactyl\Models\APIKey $key */ + DB::transaction(function () { + foreach (DB::table('api_keys')->cursor() as $key) { + DB::table('api_keys')->where('id', $key->id)->update([ + 'token' => Crypt::decrypt($key->token), + ]); + } + }); + + Schema::table('api_keys', function (Blueprint $table) { + $table->dropColumn('identifier'); + $table->string('token', 32)->unique()->change(); + }); + } +} diff --git a/database/migrations/2018_01_13_145209_AddLastUsedAtColumn.php b/database/migrations/2018_01_13_145209_AddLastUsedAtColumn.php new file mode 100644 index 000000000..83d394428 --- /dev/null +++ b/database/migrations/2018_01_13_145209_AddLastUsedAtColumn.php @@ -0,0 +1,34 @@ +timestamp('last_used_at')->after('memo')->nullable(); + $table->dropColumn('expires_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('api_keys', function (Blueprint $table) { + $table->timestamp('expires_at')->after('memo')->nullable(); + $table->dropColumn('last_used_at'); + }); + } +} diff --git a/resources/lang/en/strings.php b/resources/lang/en/strings.php index c9983e643..5b9173866 100644 --- a/resources/lang/en/strings.php +++ b/resources/lang/en/strings.php @@ -83,4 +83,5 @@ return [ 'fri' => 'Friday', 'sat' => 'Saturday', ], + 'last_used' => 'Last Used', ]; diff --git a/resources/themes/pterodactyl/base/api/index.blade.php b/resources/themes/pterodactyl/base/api/index.blade.php index 0d8fc0a3e..1a6dd0a72 100644 --- a/resources/themes/pterodactyl/base/api/index.blade.php +++ b/resources/themes/pterodactyl/base/api/index.blade.php @@ -31,26 +31,26 @@