2022-09-30 13:56:11 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App\Services\Migrators;
|
|
|
|
|
2022-11-22 15:15:52 +01:00
|
|
|
use App\Exceptions\InvalidMigrationDataException;
|
|
|
|
use App\Facades\TwoFAccounts;
|
2022-10-07 18:58:48 +02:00
|
|
|
use App\Models\TwoFAccount;
|
|
|
|
use Illuminate\Support\Arr;
|
2022-11-22 15:15:52 +01:00
|
|
|
use Illuminate\Support\Collection;
|
|
|
|
use Illuminate\Support\Facades\Log;
|
2022-09-30 13:56:11 +02:00
|
|
|
|
2022-10-07 18:58:48 +02:00
|
|
|
class AegisMigrator extends Migrator
|
2022-09-30 13:56:11 +02:00
|
|
|
{
|
2022-10-07 18:58:48 +02:00
|
|
|
// Typical JSON structure of an Aegis export
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// "type": "totp",
|
|
|
|
// "uuid": "5be1c189-240d-5fe1-930b-a78xb669zd86",
|
|
|
|
// "name": "John DOE",
|
|
|
|
// "issuer": "Facebook",
|
|
|
|
// "note": "",
|
|
|
|
// "icon": "PHN2ZyB4bWxucz0ia[...]0KPC9zdmc+DQo=",
|
|
|
|
// "icon_mime": "image\/svg+xml",
|
|
|
|
// "info": {
|
|
|
|
// "secret": "A4GRFTVVRBGY7UIW",
|
|
|
|
// "algo": "SHA1",
|
|
|
|
// "digits": 6,
|
|
|
|
// "period": 30,
|
|
|
|
// "counter": 30
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
2022-09-30 13:56:11 +02:00
|
|
|
/**
|
2022-10-07 18:58:48 +02:00
|
|
|
* Convert migration data to a TwoFAccounts collection.
|
2022-09-30 13:56:11 +02:00
|
|
|
*
|
2022-11-21 11:16:43 +01:00
|
|
|
* @return \Illuminate\Support\Collection<int|string, \App\Models\TwoFAccount> The converted accounts
|
2022-09-30 13:56:11 +02:00
|
|
|
*/
|
2022-12-13 12:07:29 +01:00
|
|
|
public function migrate(mixed $migrationPayload) : Collection
|
2022-09-30 13:56:11 +02:00
|
|
|
{
|
2022-10-07 18:58:48 +02:00
|
|
|
$json = json_decode(htmlspecialchars_decode($migrationPayload), true);
|
|
|
|
|
|
|
|
if (is_null($json) || Arr::has($json, 'db.entries') == false) {
|
|
|
|
Log::error('Aegis JSON migration data cannot be read');
|
|
|
|
throw new InvalidMigrationDataException('Aegis');
|
|
|
|
}
|
|
|
|
|
2022-11-22 15:15:52 +01:00
|
|
|
$twofaccounts = [];
|
2022-10-12 11:13:13 +02:00
|
|
|
|
2022-10-07 18:58:48 +02:00
|
|
|
foreach ($json['db']['entries'] as $key => $otp_parameters) {
|
2022-11-22 15:15:52 +01:00
|
|
|
$parameters = [];
|
|
|
|
$parameters['otp_type'] = $otp_parameters['type'] == 'steam' ? TwoFAccount::STEAM_TOTP : $otp_parameters['type'];
|
|
|
|
$parameters['service'] = $otp_parameters['issuer'];
|
2023-06-07 17:49:12 +02:00
|
|
|
$parameters['account'] = $otp_parameters['name'] ?? $parameters['service'];
|
2022-11-22 15:15:52 +01:00
|
|
|
$parameters['secret'] = $this->padToValidBase32Secret($otp_parameters['info']['secret']);
|
|
|
|
$parameters['algorithm'] = $otp_parameters['info']['algo'];
|
|
|
|
$parameters['digits'] = $otp_parameters['info']['digits'];
|
|
|
|
$parameters['counter'] = $otp_parameters['info']['counter'] ?? null;
|
|
|
|
$parameters['period'] = $otp_parameters['info']['period'] ?? null;
|
2022-10-07 18:58:48 +02:00
|
|
|
|
2022-10-10 11:24:02 +02:00
|
|
|
try {
|
|
|
|
// Aegis supports 3 image extensions for icons
|
|
|
|
// (see https://github.com/beemdevelopment/Aegis/blob/3c10b234ea70715776a09e3d200cb6e806a43f83/docs/iconpacks.md)
|
|
|
|
|
|
|
|
if (Arr::has($otp_parameters, 'icon') && Arr::has($otp_parameters, 'icon_mime')) {
|
|
|
|
switch ($otp_parameters['icon_mime']) {
|
|
|
|
case 'image/svg+xml':
|
2022-12-13 09:08:22 +01:00
|
|
|
$parameters['iconExt'] = 'svg';
|
2022-10-10 11:24:02 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'image/png':
|
2022-12-13 09:08:22 +01:00
|
|
|
$parameters['iconExt'] = 'png';
|
2022-10-10 11:24:02 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'image/jpeg':
|
2022-12-13 09:08:22 +01:00
|
|
|
$parameters['iconExt'] = 'jpg';
|
2022-10-10 11:24:02 +02:00
|
|
|
break;
|
2022-11-22 15:15:52 +01:00
|
|
|
|
2022-10-10 11:24:02 +02:00
|
|
|
default:
|
|
|
|
throw new \Exception();
|
|
|
|
}
|
2022-12-13 09:08:22 +01:00
|
|
|
$parameters['iconData'] = base64_decode($otp_parameters['icon']);
|
2022-10-10 11:24:02 +02:00
|
|
|
}
|
2022-11-22 15:15:52 +01:00
|
|
|
} catch (\Exception) {
|
2022-10-10 11:24:02 +02:00
|
|
|
// we do nothing
|
|
|
|
}
|
|
|
|
|
2022-10-07 18:58:48 +02:00
|
|
|
try {
|
2022-11-22 15:15:52 +01:00
|
|
|
$twofaccounts[$key] = new TwoFAccount;
|
|
|
|
$twofaccounts[$key]->fillWithOtpParameters($parameters);
|
2022-12-13 09:08:22 +01:00
|
|
|
if (Arr::has($parameters, 'iconExt') && Arr::has($parameters, 'iconData')) {
|
|
|
|
$twofaccounts[$key]->setIcon($parameters['iconData'], $parameters['iconExt']);
|
|
|
|
}
|
2022-11-22 15:15:52 +01:00
|
|
|
} catch (\Exception $exception) {
|
2022-10-07 18:58:48 +02:00
|
|
|
Log::error(sprintf('Cannot instanciate a TwoFAccount object with OTP parameters from imported item #%s', $key));
|
2022-12-09 10:55:39 +01:00
|
|
|
Log::debug($exception->getMessage());
|
2022-10-07 18:58:48 +02:00
|
|
|
|
|
|
|
// The token failed to generate a valid account so we create a fake account to be returned.
|
2022-11-22 15:15:52 +01:00
|
|
|
$fakeAccount = new TwoFAccount();
|
|
|
|
$fakeAccount->id = TwoFAccount::FAKE_ID;
|
|
|
|
$fakeAccount->otp_type = $otp_parameters['type'] ?? TwoFAccount::TOTP;
|
2022-10-07 18:58:48 +02:00
|
|
|
// Only basic fields are filled to limit the risk of another exception.
|
2022-11-22 15:15:52 +01:00
|
|
|
$fakeAccount->account = $otp_parameters['name'] ?? __('twofaccounts.import.invalid_account');
|
|
|
|
$fakeAccount->service = $otp_parameters['issuer'] ?? __('twofaccounts.import.invalid_service');
|
2022-10-07 18:58:48 +02:00
|
|
|
// The secret field is used to pass the error, not very clean but will do the job for now.
|
2022-11-22 15:15:52 +01:00
|
|
|
$fakeAccount->secret = $exception->getMessage();
|
2022-10-07 18:58:48 +02:00
|
|
|
|
|
|
|
$twofaccounts[$key] = $fakeAccount;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return collect($twofaccounts);
|
2022-09-30 13:56:11 +02:00
|
|
|
}
|
|
|
|
}
|