From ba636a95dc4ca5d27711e79756840e78cbd2d1a6 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:24:42 +0200 Subject: [PATCH] Refactor SSH Keys --- app/Livewire/Security/PrivateKey/Create.php | 107 ++++++-------- app/Models/PrivateKey.php | 132 +++++++++++++++--- .../security/private-key/create.blade.php | 4 +- 3 files changed, 154 insertions(+), 89 deletions(-) diff --git a/app/Livewire/Security/PrivateKey/Create.php b/app/Livewire/Security/PrivateKey/Create.php index 1a689076b..95f6a71bf 100644 --- a/app/Livewire/Security/PrivateKey/Create.php +++ b/app/Livewire/Security/PrivateKey/Create.php @@ -3,22 +3,14 @@ namespace App\Livewire\Security\PrivateKey; use App\Models\PrivateKey; -use DanHarrin\LivewireRateLimiting\WithRateLimiting; use Livewire\Component; -use phpseclib3\Crypt\PublicKeyLoader; class Create extends Component { - use WithRateLimiting; - - public string $name; - - public string $value; - + public string $name = ''; + public string $value = ''; public ?string $from = null; - public ?string $description = null; - public ?string $publicKey = null; protected $rules = [ @@ -26,84 +18,69 @@ class Create extends Component 'value' => 'required|string', ]; - protected $validationAttributes = [ - 'name' => 'name', - 'value' => 'private Key', - ]; - public function generateNewRSAKey() { - try { - $this->rateLimit(10); - $this->name = generate_random_name(); - $this->description = 'Created by Coolify'; - ['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey(); - } catch (\Throwable $e) { - return handleError($e, $this); - } + $this->generateNewKey('rsa'); } public function generateNewEDKey() { - try { - $this->rateLimit(10); - $this->name = generate_random_name(); - $this->description = 'Created by Coolify'; - ['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey('ed25519'); - } catch (\Throwable $e) { - return handleError($e, $this); - } + $this->generateNewKey('ed25519'); } - public function updated($updateProperty) + private function generateNewKey($type) { - if ($updateProperty === 'value') { - try { - $key = PublicKeyLoader::load($this->$updateProperty); - $this->publicKey = $key->getPublicKey()->toString('OpenSSH', ['comment' => '']); - } catch (\Throwable $e) { - $this->publicKey = ''; - $this->addError('value', 'Invalid private key'); - } + $keyData = PrivateKey::generateNewKeyPair($type); + $this->setKeyData($keyData); + } + + public function updated($property) + { + if ($property === 'value') { + $this->validatePrivateKey(); } - $this->validateOnly($updateProperty); } public function createPrivateKey() { - $this->validate([ - 'name' => 'required|string', - 'value' => [ - 'required', - 'string', - function ($attribute, $value, $fail) { - try { - PublicKeyLoader::load($value); - } catch (\Throwable $e) { - $fail('The private key is invalid.'); - } - }, - ], - ]); + $this->validate(); try { - $this->value = trim($this->value); - if (! str_ends_with($this->value, "\n")) { - $this->value .= "\n"; - } - $private_key = PrivateKey::create([ + $privateKey = PrivateKey::createAndStore([ 'name' => $this->name, 'description' => $this->description, - 'private_key' => $this->value, + 'private_key' => trim($this->value) . "\n", 'team_id' => currentTeam()->id, ]); - if ($this->from === 'server') { - return redirect()->route('dashboard'); - } - return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]); + return $this->redirectAfterCreation($privateKey); } catch (\Throwable $e) { return handleError($e, $this); } } + + private function setKeyData(array $keyData) + { + $this->name = $keyData['name']; + $this->description = $keyData['description']; + $this->value = $keyData['private_key']; + $this->publicKey = $keyData['public_key']; + } + + private function validatePrivateKey() + { + $validationResult = PrivateKey::validateAndExtractPublicKey($this->value); + $this->publicKey = $validationResult['publicKey']; + + if (!$validationResult['isValid']) { + $this->addError('value', 'Invalid private key'); + } + } + + private function redirectAfterCreation(PrivateKey $privateKey) + { + return $this->from === 'server' + ? redirect()->route('dashboard') + : redirect()->route('security.private-key.show', ['private_key_uuid' => $privateKey->uuid]); + } } diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php index 7cb58657c..a56e67ff6 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -3,8 +3,10 @@ namespace App\Models; use OpenApi\Attributes as OA; -use phpseclib3\Crypt\PublicKeyLoader; +use Illuminate\Support\Facades\Storage; use Illuminate\Validation\ValidationException; +use phpseclib3\Crypt\PublicKeyLoader; +use DanHarrin\LivewireRateLimiting\WithRateLimiting; #[OA\Schema( description: 'Private Key model', @@ -23,6 +25,8 @@ use Illuminate\Validation\ValidationException; )] class PrivateKey extends BaseModel { + use WithRateLimiting; + protected $fillable = [ 'name', 'description', @@ -39,45 +43,127 @@ class PrivateKey extends BaseModel protected static function booted() { static::saving(function ($key) { - $privateKey = data_get($key, 'private_key'); - if (substr($privateKey, -1) !== "\n") { - $key->private_key = $privateKey . "\n"; - } + $key->private_key = rtrim($key->private_key) . "\n"; - try { - $publicKey = PublicKeyLoader::load($key->private_key)->getPublicKey(); - $key->fingerprint = $publicKey->getFingerprint('sha256'); - } catch (\Throwable $e) { + if (!self::validatePrivateKey($key->private_key)) { throw ValidationException::withMessages([ 'private_key' => ['The private key is invalid.'], ]); } + + $key->fingerprint = self::generateFingerprint($key->private_key); }); + + static::deleted(function ($key) { + self::deleteFromStorage($key); + }); + } + + public function getPublicKey() + { + return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key'; + } + + // For backwards compatibility + public function publicKey() + { + return $this->getPublicKey(); } public static function ownedByCurrentTeam(array $select = ['*']) { $selectArray = collect($select)->concat(['id']); - - return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all()); + return self::whereTeamId(currentTeam()->id)->select($selectArray->all()); } - public function publicKey() + public static function validatePrivateKey($privateKey) { try { - return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH', ['comment' => '']); + PublicKeyLoader::load($privateKey); + return true; } catch (\Throwable $e) { - return 'Error loading private key'; + return false; } } - public function isEmpty() + public static function generateFingerprint($privateKey) { - if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) { - return true; - } + $key = PublicKeyLoader::load($privateKey); + return $key->getPublicKey()->getFingerprint('sha256'); + } - return false; + public static function createAndStore(array $data) + { + $privateKey = new self($data); + $privateKey->save(); + $privateKey->storeInFileSystem(); + return $privateKey; + } + + public static function generateNewKeyPair($type = 'rsa') + { + try { + $instance = new self(); + $instance->rateLimit(10); + $name = generate_random_name(); + $description = 'Created by Coolify'; + ['private' => $privateKey, 'public' => $publicKey] = generateSSHKey($type === 'ed25519' ? 'ed25519' : 'rsa'); + + return [ + 'name' => $name, + 'description' => $description, + 'private_key' => $privateKey, + 'public_key' => $publicKey, + ]; + } catch (\Throwable $e) { + throw new \Exception("Failed to generate new {$type} key: " . $e->getMessage()); + } + } + + public static function extractPublicKeyFromPrivate($privateKey) + { + try { + $key = PublicKeyLoader::load($privateKey); + return $key->getPublicKey()->toString('OpenSSH', ['comment' => '']); + } catch (\Throwable $e) { + return null; + } + } + + public static function validateAndExtractPublicKey($privateKey) + { + $isValid = self::validatePrivateKey($privateKey); + $publicKey = $isValid ? self::extractPublicKeyFromPrivate($privateKey) : ''; + + return [ + 'isValid' => $isValid, + 'publicKey' => $publicKey, + ]; + } + + public function storeInFileSystem() + { + $filename = "id_rsa@{$this->uuid}"; + Storage::disk('ssh-keys')->put($filename, $this->private_key); + return "/var/www/html/storage/app/ssh/keys/{$filename}"; + } + + public static function deleteFromStorage(self $privateKey) + { + $filename = "id_rsa@{$privateKey->uuid}"; + Storage::disk('ssh-keys')->delete($filename); + } + + public function getKeyLocation() + { + return "/var/www/html/storage/app/ssh/keys/id_rsa@{$this->uuid}"; + } + + public function updatePrivateKey(array $data) + { + $this->update($data); + $this->storeInFileSystem(); + return $this; } public function servers() @@ -100,9 +186,11 @@ class PrivateKey extends BaseModel return $this->hasMany(GitlabApp::class); } - public function generateFingerprint() + public function isEmpty() { - $key = PublicKeyLoader::load($this->private_key); - return $key->getPublicKey()->getFingerprint('sha256'); + return $this->servers()->count() === 0 + && $this->applications()->count() === 0 + && $this->githubApps()->count() === 0 + && $this->gitlabApps()->count() === 0; } } diff --git a/resources/views/livewire/security/private-key/create.blade.php b/resources/views/livewire/security/private-key/create.blade.php index 1bace9f3a..a57cfc8e7 100644 --- a/resources/views/livewire/security/private-key/create.blade.php +++ b/resources/views/livewire/security/private-key/create.blade.php @@ -4,8 +4,8 @@