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 }