mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-06-25 22:41:57 +02:00
Add support for 2FAuth json migration
This commit is contained in:
parent
f661a0bc17
commit
17137b9885
@ -17,10 +17,15 @@ class TwoFAccountExportCollection extends ResourceCollection
|
|||||||
* Transform the resource collection into an array.
|
* Transform the resource collection into an array.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @return \Illuminate\Support\Collection<int|string, TwoFAccountExportResource>
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function toArray($request)
|
public function toArray($request)
|
||||||
{
|
{
|
||||||
return $this->collection;
|
return [
|
||||||
|
'app' => '2fauth_v' . config('2fauth.version'),
|
||||||
|
'schema' => 1,
|
||||||
|
'datetime' => now(),
|
||||||
|
'data' => $this->collection,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ use App\Services\Migrators\GoogleAuthMigrator;
|
|||||||
use App\Services\Migrators\Migrator;
|
use App\Services\Migrators\Migrator;
|
||||||
use App\Services\Migrators\PlainTextMigrator;
|
use App\Services\Migrators\PlainTextMigrator;
|
||||||
use App\Services\Migrators\TwoFASMigrator;
|
use App\Services\Migrators\TwoFASMigrator;
|
||||||
|
use App\Services\Migrators\TwoFAuthMigrator;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Facades\App;
|
use Illuminate\Support\Facades\App;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
@ -23,7 +24,9 @@ class MigratorFactory implements MigratorFactoryInterface
|
|||||||
*/
|
*/
|
||||||
public function create(string $migrationPayload) : Migrator
|
public function create(string $migrationPayload) : Migrator
|
||||||
{
|
{
|
||||||
if ($this->isAegisJSON($migrationPayload)) {
|
if ($this->isTwoFAuthJSON($migrationPayload)) {
|
||||||
|
return App::make(TwoFAuthMigrator::class);
|
||||||
|
} elseif ($this->isAegisJSON($migrationPayload)) {
|
||||||
return App::make(AegisMigrator::class);
|
return App::make(AegisMigrator::class);
|
||||||
} elseif ($this->is2FASv2($migrationPayload)) {
|
} elseif ($this->is2FASv2($migrationPayload)) {
|
||||||
return App::make(TwoFASMigrator::class);
|
return App::make(TwoFASMigrator::class);
|
||||||
@ -73,6 +76,35 @@ class MigratorFactory implements MigratorFactoryInterface
|
|||||||
)->passes();
|
)->passes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a payload comes from 2FAuth in JSON format
|
||||||
|
*
|
||||||
|
* @param string $migrationPayload The payload to analyse
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function isTwoFAuthJSON(string $migrationPayload) : bool
|
||||||
|
{
|
||||||
|
$json = json_decode($migrationPayload, true);
|
||||||
|
|
||||||
|
if (Arr::has($json, 'schema') && (strpos(Arr::get($json, 'app'), '2fauth_') === 0)) {
|
||||||
|
return count(Validator::validate(
|
||||||
|
$json,
|
||||||
|
[
|
||||||
|
'data.*.otp_type' => 'required',
|
||||||
|
'data.*.service' => 'required',
|
||||||
|
'data.*.account' => 'required',
|
||||||
|
'data.*.secret' => 'required',
|
||||||
|
'data.*.digits' => 'required',
|
||||||
|
'data.*.algorithm' => 'required',
|
||||||
|
'data.*.period' => 'present',
|
||||||
|
'data.*.counter' => 'present',
|
||||||
|
]
|
||||||
|
)) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if a payload comes from Aegis Authenticator in JSON format
|
* Determine if a payload comes from Aegis Authenticator in JSON format
|
||||||
*
|
*
|
||||||
|
131
app/Services/Migrators/TwoFAuthMigrator.php
Normal file
131
app/Services/Migrators/TwoFAuthMigrator.php
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Migrators;
|
||||||
|
|
||||||
|
use App\Exceptions\InvalidMigrationDataException;
|
||||||
|
use App\Models\TwoFAccount;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class TwoFAuthMigrator extends Migrator
|
||||||
|
{
|
||||||
|
// {
|
||||||
|
// "app": "2fauth_v3.4.1",
|
||||||
|
// "schema": 1,
|
||||||
|
// "datetime": "2022-12-14T14:53:06.173939Z",
|
||||||
|
// "data":
|
||||||
|
// [
|
||||||
|
// {
|
||||||
|
// "otp_type": "totp",
|
||||||
|
// "account": "cwxcwxc",
|
||||||
|
// "service": "wcxwxcwx",
|
||||||
|
// "icon": null,
|
||||||
|
// "icon_mime": null,
|
||||||
|
// "icon_file": null,
|
||||||
|
// "secret": "EEEE====",
|
||||||
|
// "digits": 6,
|
||||||
|
// "algorithm": "sha1",
|
||||||
|
// "period": 30,
|
||||||
|
// "counter": null,
|
||||||
|
// "legacy_uri": "otpauth://totp/wcxwxcwx%3Acwxcwxc?issuer=wcxwxcwx&secret=EEEE"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert migration data to a TwoFAccounts collection.
|
||||||
|
*
|
||||||
|
* @param mixed $migrationPayload
|
||||||
|
* @return \Illuminate\Support\Collection<int|string, \App\Models\TwoFAccount> The converted accounts
|
||||||
|
*/
|
||||||
|
public function migrate(mixed $migrationPayload) : Collection
|
||||||
|
{
|
||||||
|
$json = json_decode(htmlspecialchars_decode($migrationPayload), true);
|
||||||
|
|
||||||
|
if (is_null($json)) {
|
||||||
|
Log::error('2FAuth JSON migration data cannot be read');
|
||||||
|
throw new InvalidMigrationDataException('2FAS Auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
$twofaccounts = [];
|
||||||
|
|
||||||
|
foreach ($json['data'] as $key => $otp_parameters) {
|
||||||
|
$parameters = [];
|
||||||
|
$parameters['otp_type'] = $otp_parameters['otp_type'];
|
||||||
|
$parameters['service'] = $otp_parameters['service'];
|
||||||
|
$parameters['account'] = $otp_parameters['account'];
|
||||||
|
$parameters['secret'] = $this->padToValidBase32Secret($otp_parameters['secret']);
|
||||||
|
$parameters['algorithm'] = $otp_parameters['algorithm'];
|
||||||
|
$parameters['digits'] = $otp_parameters['digits'];
|
||||||
|
$parameters['legacy_uri'] = $otp_parameters['legacy_uri'];
|
||||||
|
$parameters['counter'] = strtolower($parameters['otp_type']) === 'hotp' && $otp_parameters['counter'] > 0
|
||||||
|
? $otp_parameters['counter']
|
||||||
|
: null;
|
||||||
|
$parameters['period'] = strtolower($parameters['otp_type']) === 'totp' && $otp_parameters['period'] > 0
|
||||||
|
? $otp_parameters['period']
|
||||||
|
: null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Arr::has($otp_parameters, 'icon_file') && Arr::has($otp_parameters, 'icon_mime')) {
|
||||||
|
switch ($otp_parameters['icon_mime']) {
|
||||||
|
case 'image/svg+xml':
|
||||||
|
$parameters['iconExt'] = 'svg';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'image/png':
|
||||||
|
$parameters['iconExt'] = 'png';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'image/jpeg':
|
||||||
|
$parameters['iconExt'] = 'jpg';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'image/bmp':
|
||||||
|
$parameters['iconExt'] = 'bmp';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'image/x-ms-bmp':
|
||||||
|
$parameters['iconExt'] = 'bmp';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'image/webp':
|
||||||
|
$parameters['iconExt'] = 'webp';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new \Exception();
|
||||||
|
}
|
||||||
|
$parameters['icon_file'] = base64_decode($otp_parameters['icon_file']);
|
||||||
|
}
|
||||||
|
} catch (\Exception) {
|
||||||
|
// we do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$twofaccounts[$key] = new TwoFAccount;
|
||||||
|
$twofaccounts[$key]->fillWithOtpParameters($parameters);
|
||||||
|
if (Arr::has($parameters, 'iconExt')) {
|
||||||
|
$twofaccounts[$key]->setIcon($parameters['icon_file'], $parameters['iconExt']);
|
||||||
|
}
|
||||||
|
} catch (\Exception $exception) {
|
||||||
|
Log::error(sprintf('Cannot instanciate a TwoFAccount object with 2FAS imported item #%s', $key));
|
||||||
|
Log::debug($exception->getMessage());
|
||||||
|
|
||||||
|
// The token failed to generate a valid account so we create a fake account to be returned.
|
||||||
|
$fakeAccount = new TwoFAccount();
|
||||||
|
$fakeAccount->id = TwoFAccount::FAKE_ID;
|
||||||
|
$fakeAccount->otp_type = $otp_parameters['otp']['tokenType'] ?? TwoFAccount::TOTP;
|
||||||
|
// Only basic fields are filled to limit the risk of another exception.
|
||||||
|
$fakeAccount->account = $otp_parameters['otp']['account'] ?? __('twofaccounts.import.invalid_account');
|
||||||
|
$fakeAccount->service = $otp_parameters['name'] ?? __('twofaccounts.import.invalid_service');
|
||||||
|
// The secret field is used to pass the error, not very clean but will do the job for now.
|
||||||
|
$fakeAccount->secret = $exception->getMessage();
|
||||||
|
|
||||||
|
$twofaccounts[$key] = $fakeAccount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($twofaccounts);
|
||||||
|
}
|
||||||
|
}
|
@ -41,6 +41,12 @@
|
|||||||
<!-- Supported migration resources -->
|
<!-- Supported migration resources -->
|
||||||
<h5 class="title is-5 mb-3 has-text-grey-dark">{{ $t('twofaccounts.import.supported_migration_formats') }}</h5>
|
<h5 class="title is-5 mb-3 has-text-grey-dark">{{ $t('twofaccounts.import.supported_migration_formats') }}</h5>
|
||||||
<div class="field is-grouped is-grouped-multiline pt-0">
|
<div class="field is-grouped is-grouped-multiline pt-0">
|
||||||
|
<div class="control">
|
||||||
|
<div class="tags has-addons">
|
||||||
|
<span class="tag is-dark">2FAuth</span>
|
||||||
|
<span class="tag is-black">JSON</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<div class="tags has-addons">
|
<div class="tags has-addons">
|
||||||
<span class="tag is-dark">Google Auth</span>
|
<span class="tag is-dark">Google Auth</span>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user