/ app / Http / Services / GpgService.php
GpgService.php
  1  <?php
  2  
  3  namespace App\Http\Services;
  4  
  5  use App\Enums\EntityTypeEnum;
  6  use App\Enums\UserTypeEnum;
  7  use App\Models\Entity;
  8  use App\Models\GpgKey;
  9  use App\Models\Revision;
 10  use App\Models\User;
 11  use Carbon\Carbon;
 12  use Crypt_GPG;
 13  use Crypt_GPG_Exception;
 14  use Crypt_GPG_KeyGenerator;
 15  use Crypt_GPG_KeyNotFoundException;
 16  use Crypt_GPG_SubKey;
 17  use Crypt_GPG_UserId;
 18  use Exception;
 19  use Illuminate\Database\Eloquent\ModelNotFoundException;
 20  use Illuminate\Support\Facades\Storage;
 21  use Illuminate\Support\Str;
 22  
 23  class GpgService
 24  {
 25      public const PUBLIC_KEY_PATH = 'tmp/public_key.asc';
 26      public const PRIVATE_KEY_PATH = 'tmp/private_key.asc';
 27      private Crypt_GPG $gpg;
 28      private int $expirationYears = 1;
 29      private int $keySize = 4096;
 30  
 31      public function __construct()
 32      {
 33          $this->gpg = new Crypt_GPG(['homedir' => config('gpg.homedir')]);
 34          unset($_ENV['argv']);
 35      }
 36  
 37      /**
 38       * @return string
 39       * @throws Crypt_GPG_Exception
 40       * @throws Crypt_GPG_KeyNotFoundException
 41       * @throws ModelNotFoundException
 42       */
 43      public function getPublicKey(): string
 44      {
 45          $cab = Entity::where('entity_type_id', EntityTypeEnum::cab())->first();
 46          return $this->gpg->exportPublicKey($cab->keys->first()->key_id);
 47      }
 48  
 49      protected function generateKeyPair(string $name, string $email, string $passphrase): array
 50      {
 51          $userKeyId = new Crypt_GPG_UserId(
 52              [
 53                  'name' => $name,
 54                  'email' => $email,
 55              ]
 56          );
 57          $creationDate = Carbon::now();
 58          $expirationDate = Carbon::now()->endOfYear()->addYears($this->expirationYears);
 59          $generator = new Crypt_GPG_KeyGenerator([
 60              'binary' => config('gpg.binary'),
 61              'homedir' => config('gpg.homedir'),
 62              'debug' => false,
 63          ]);
 64          $generator->setKeyParams(Crypt_GPG_SubKey::ALGORITHM_RSA, $this->keySize, 15);
 65          $generator->setSubKeyParams(Crypt_GPG_SubKey::ALGORITHM_RSA, $this->keySize, 15);
 66          $generator->setPassphrase($passphrase);
 67          $key = $generator->setExpirationDate($expirationDate->timestamp)->generateKey($userKeyId);
 68  
 69          return [
 70              'key_id' => $key->__toString(),
 71              'key_fingerprint' => $this->gpg->getFingerprint($key->__toString()),
 72              'key_created_at' => $creationDate,
 73              'key_expiration_at' => $expirationDate,
 74              'key_algorithm' => 1,
 75              'key_length' => $this->keySize,
 76              'key_name' => $name,
 77              'key_email' => $email,
 78              'key_comment' => '',
 79              'key_passphrase' => $passphrase,
 80          ];
 81      }
 82  
 83      protected function saveKeyInDatabase(array $keyData): GpgKey
 84      {
 85          $cab = Entity::where('entity_type_id', EntityTypeEnum::cab())->first();
 86  
 87          if ($cab->keys) {
 88              $cab->keys()->delete();
 89          }
 90  
 91          return $cab->keys()->create($keyData);
 92      }
 93  
 94      public function createTmpPublicKey(): void
 95      {
 96          $cab = Entity::where('entity_type_id', EntityTypeEnum::cab())->first();
 97          $data = $this->gpg->exportPublicKey($cab->keys->first()->key_id);
 98          Storage::disk('local')->put(self::PUBLIC_KEY_PATH, $data);
 99      }
100  
101      public function createTmpPrivateKey(): void
102      {
103          $cab = Entity::where('entity_type_id', EntityTypeEnum::cab())->first();
104          $key = $cab->keys->first();
105          $key_id = $key->key_id;
106          $passphrase = $key->key_passphrase;
107  
108          $this->gpg->addPassphrase($key_id, $passphrase);
109          $data = $this->gpg->exportPrivateKey($key_id);
110          Storage::disk('local')->put(self::PRIVATE_KEY_PATH, $data);
111      }
112  
113      public function decryptAndVerify(string $encryptedMessage): string
114      {
115          $cab = Entity::where('entity_type_id', EntityTypeEnum::cab())->first();
116          $key = $cab->keys->first();
117  
118          if (!$key) {
119              throw new Exception(__('dossiers.dossier.notifications.create.error.decrypt'));
120          }
121  
122          try {
123              $this->gpg->addDecryptKey($key->key_id, $key->key_passphrase);
124              $data = $this->gpg->decrypt($encryptedMessage);
125          } catch (Exception $e) {
126              throw new Exception(__('dossiers.dossier.notifications.create.error.decrypt'));
127          }
128  
129          return $data;
130      }
131  
132      public function addPublicKeys(array $publicKeys = []): void
133      {
134          foreach ($publicKeys as $r) {
135              $this->gpg->addEncryptKey($r);
136          }
137      }
138  
139      /**
140       * @throws Crypt_GPG_Exception
141       * @throws \Crypt_GPG_FileException
142       * @throws Crypt_GPG_KeyNotFoundException
143       */
144      public function encrypt($blob): ?string
145      {
146          return $this->gpg->encrypt($blob, false);
147      }
148  
149      /**
150       * @throws Crypt_GPG_Exception
151       * @throws Crypt_GPG_KeyNotFoundException
152       * @throws \Crypt_GPG_FileException
153       */
154      public function encryptFromPath(string $fullPath): string
155      {
156          return $this->gpg->encryptFile($fullPath, null, false);
157      }
158  
159      public function generateAllUsersKeys(): void
160      {
161          $users = User::where('user_type_id', UserTypeEnum::INTERNAL->value)->get();
162          foreach ($users as $model) {
163              $this->generateUserKey($model);
164          }
165      }
166  
167      public static function generateSecurePassword(int $wordCount = 4, string $joinLetter = '-', bool $capitalize = true, bool $addNumber = true): string
168      {
169          $dictionary = Storage::disk('local')->get('dict/diccionario.txt');
170          $words = explode("\n", trim($dictionary)); 
171          $words = array_map(function($word) {
172              $word = preg_replace('/[\r\n\x00]/', '', $word);
173              return trim($word);
174          }, $words);
175          
176          $words = array_filter($words);
177          
178          if (empty($words)) {
179              return Str::random(16);
180          }
181          
182          $selectedWords = [];
183          
184          for ($i = 0; $i < $wordCount; $i++) {
185              $word = $words[array_rand($words)];
186              $selectedWords[] = $capitalize ? ucfirst($word) : $word;
187          }
188          
189          // Add a random number to one of the words
190          if ($addNumber) {
191              $randomNumber = rand(1, 99);
192              $wordToAddNumber = rand(0, $wordCount - 1);
193              $selectedWords[$wordToAddNumber] .= $randomNumber;
194          }
195          
196          return implode($joinLetter, $selectedWords);
197      }
198  
199      public function generateUserKey(User $user, string $passphrase = null): void
200      {
201          if ($user->activeKey) {
202              $user->activeKey()->delete();
203          }
204  
205          $keyData = $this->generateKeyPair($user->name, $user->email, $passphrase ?? $this->generateSecurePassword());
206          $user->activeKey()->create($keyData);
207      }
208  
209      public function generateAppKey(): GpgKey
210      {
211          $keyData = $this->generateKeyPair(
212              config('gpg.certificate.user'),
213              config('gpg.certificate.email'),
214              $this->generateSecurePassword()
215          );
216  
217          $cab = Entity::where('entity_type_id', EntityTypeEnum::cab())->first();
218          if ($cab->keys) {
219              $cab->keys()->delete();
220          }
221  
222          return $cab->keys()->create($keyData);
223      }
224  
225      public function generateEntityKey(Entity $entity): GpgKey
226      {
227          $keyData = $this->generateKeyPair(
228              $entity->name,
229              $entity->contactInfo->email,
230              $this->generateSecurePassword()
231          );
232  
233          if ($entity->keys) {
234              $entity->keys()->delete();
235          }
236  
237          return $entity->keys()->create($keyData);
238      }
239  
240      public function signFile(User $user, string $passphrase, Revision $toSign, string $destination): string|bool
241      {
242          $destination .= '.pgp';
243          $this->gpg->addSignKey($user->activeKey->key_id, $passphrase);
244          $this->gpg->signFile(
245              Storage::path($toSign->path),
246              Storage::path($destination),
247              Crypt_GPG::SIGN_MODE_NORMAL
248          );
249          $this->gpg->clearSignKeys();
250          return $destination;
251      }
252  
253      public function verifyFile(string $filePath): bool
254      {
255          foreach ($this->gpg->verifyFile($filePath) as $signature) {
256              if (!$signature->isValid()) {
257                  return false;
258              }
259          }
260          return true;
261      }
262  
263      public function exportPublic(GpgKey $key): string
264      {
265          $tmp = 'tmp/' . uniqid();
266          Storage::put($tmp, $this->gpg->exportPublicKey($key->key_id));
267          return $tmp;
268      }
269  
270      public function exportPrivate(GpgKey $key): string
271      {
272          $tmp = 'tmp/' . uniqid();
273          $this->gpg->addPassphrase($key->key_id, $key->key_passphrase);
274          Storage::put($tmp, $this->gpg->exportPrivateKey($key->key_id));
275          return $tmp;
276      }
277  
278      public function generatePublicKey(User $user): string
279      {
280          if (!Storage::exists('/gpg/public_keys/' . $user->activeKey->key_id . '.asc')) {
281              Storage::put('/gpg/public_keys/' . $user->activeKey->key_id . '.asc', $this->gpg->exportPublicKey($user->activeKey->key_id, true));
282          }
283          return '/gpg/public_keys/' . $user->activeKey->key_id . '.asc';
284      }
285  
286      public function generatePrivateKey(User $user): string
287      {
288          if (!Storage::exists('/gpg/private_keys/' . $user->activeKey->key_id . '.asc')) {
289              $this->gpg->addPassphrase($user->activeKey->key_id, $user->activeKey->key_passphrase);
290              Storage::put('/gpg/private_keys/' . $user->activeKey->key_id . '.asc', $this->gpg->exportPrivateKey($user->activeKey->key_id, true));
291          }
292          return '/gpg/private_keys/' . $user->activeKey->key_id . '.asc';
293      }
294  
295      public static function getActiveKeys(array $usersIds): array
296      {
297          $participantsWithGpg = User::whereIn('id', $usersIds)
298              ->whereHas('activeKey')
299              ->with('activeKey')
300              ->get();
301  
302          if (config('app.env') == "production") {
303              unset($_ENV['argv']);
304          }
305          $gpgs = [];
306  
307          foreach ($participantsWithGpg as $user) {
308              /** @var $user User */
309              $gpgs[] = $user->activeKey->key_id;
310          }
311  
312          return $gpgs;
313      }
314  }