mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-06-19 19:28:08 +02:00
Add support for 2FAS Auth export and fix some issues with migrators
This commit is contained in:
parent
f867bd3fc5
commit
ed19b482cd
@ -6,6 +6,7 @@ use App\Services\Migrators\GoogleAuthMigrator;
|
||||
use App\Services\Migrators\AegisMigrator;
|
||||
use App\Services\Migrators\Migrator;
|
||||
use App\Services\Migrators\PlainTextMigrator;
|
||||
use App\Services\Migrators\TwoFASMigrator;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
@ -25,6 +26,9 @@ class MigratorFactory implements MigratorFactoryInterface
|
||||
if ($this->isAegisJSON($migrationPayload)) {
|
||||
return App::make(AegisMigrator::class);
|
||||
}
|
||||
else if ($this->is2FASv2($migrationPayload)) {
|
||||
return App::make(TwoFASMigrator::class);
|
||||
}
|
||||
else if ($this->isGoogleAuth($migrationPayload)) {
|
||||
return App::make(GoogleAuthMigrator::class);
|
||||
}
|
||||
@ -111,4 +115,52 @@ class MigratorFactory implements MigratorFactoryInterface
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function is2FASv2($migrationPayload) : mixed
|
||||
{
|
||||
// - 2FAS JSON : is a JSON object with the key 'schemaVersion' == 2 and a key 'services' full of objects like
|
||||
// {
|
||||
// "secret": "A4GRFTVVRBGY7UIW",
|
||||
// ...
|
||||
// "otp":
|
||||
// {
|
||||
// "account": "John DOE",
|
||||
// "digits": 6,
|
||||
// "counter": 0,
|
||||
// "period": 30,
|
||||
// "algorithm": "SHA1",
|
||||
// "tokenType": "TOTP"
|
||||
// },
|
||||
// "type": "ManuallyAdded",
|
||||
// "name": "Facebook",
|
||||
// "icon":
|
||||
// {
|
||||
// ...
|
||||
// }
|
||||
// }
|
||||
|
||||
$json = json_decode($migrationPayload, true);
|
||||
|
||||
if (Arr::get($json, 'schemaVersion') == 2 && (Arr::has($json, 'services') || Arr::has($json, 'servicesEncrypted'))) {
|
||||
if (Arr::has($json, 'servicesEncrypted')) {
|
||||
throw new EncryptedMigrationException();
|
||||
}
|
||||
else {
|
||||
return count(Validator::validate(
|
||||
$json,
|
||||
[
|
||||
'services.*.secret' => 'required',
|
||||
'services.*.name' => 'required',
|
||||
'services.*.otp' => 'required'
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -365,7 +365,7 @@ class TwoFAccount extends Model implements Sortable
|
||||
*/
|
||||
public function fillWithOtpParameters(array $parameters, bool $skipIconFetching = false)
|
||||
{
|
||||
$this->otp_type = Arr::get($parameters, 'otp_type');
|
||||
$this->otp_type = strtolower(Arr::get($parameters, 'otp_type'));
|
||||
$this->account = Arr::get($parameters, 'account');
|
||||
$this->service = Arr::get($parameters, 'service');
|
||||
$this->icon = Arr::get($parameters, 'icon');
|
||||
|
@ -7,6 +7,7 @@ use App\Factories\MigratorFactoryInterface;
|
||||
use App\Services\Migrators\GoogleAuthMigrator;
|
||||
use App\Services\Migrators\AegisMigrator;
|
||||
use App\Services\Migrators\PlainTextMigrator;
|
||||
use App\Services\Migrators\TwoFASMigrator;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class MigrationServiceProvider extends ServiceProvider
|
||||
@ -28,6 +29,10 @@ class MigrationServiceProvider extends ServiceProvider
|
||||
return new AegisMigrator();
|
||||
});
|
||||
|
||||
$this->app->singleton(TwoFASMigrator::class, function () {
|
||||
return new TwoFASMigrator();
|
||||
});
|
||||
|
||||
$this->app->singleton(PlainTextMigrator::class, function () {
|
||||
return new PlainTextMigrator();
|
||||
});
|
||||
|
@ -55,7 +55,7 @@ class AegisMigrator extends Migrator
|
||||
$parameters['otp_type'] = $otp_parameters['type'] == 'steam' ? TwoFAccount::STEAM_TOTP : $otp_parameters['type'];
|
||||
$parameters['service'] = $otp_parameters['issuer'];
|
||||
$parameters['account'] = $otp_parameters['name'];
|
||||
$parameters['secret'] = $otp_parameters['info']['secret'];
|
||||
$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;
|
||||
@ -108,10 +108,10 @@ class AegisMigrator extends Migrator
|
||||
// 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['type'];
|
||||
$fakeAccount->otp_type = $otp_parameters['type'] ?? TwoFAccount::TOTP;
|
||||
// Only basic fields are filled to limit the risk of another exception.
|
||||
$fakeAccount->account = $otp_parameters['name'];
|
||||
$fakeAccount->service = $otp_parameters['issuer'];
|
||||
$fakeAccount->account = $otp_parameters['name'] ?? __('twofaccounts.import.invalid_account');
|
||||
$fakeAccount->service = $otp_parameters['issuer'] ?? __('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();
|
||||
|
||||
|
@ -69,8 +69,8 @@ class GoogleAuthMigrator extends Migrator
|
||||
$fakeAccount->id = -2;
|
||||
$fakeAccount->otp_type = $fakeAccount::TOTP;
|
||||
// Only basic fields are filled to limit the risk of another exception.
|
||||
$fakeAccount->account = $otp_parameters->getName();
|
||||
$fakeAccount->service = $otp_parameters->getIssuer();
|
||||
$fakeAccount->account = $otp_parameters->getName() ?? __('twofaccounts.import.invalid_account');
|
||||
$fakeAccount->service = $otp_parameters->getIssuer() ?? __('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();
|
||||
|
||||
|
@ -14,4 +14,13 @@ abstract class Migrator
|
||||
*/
|
||||
abstract protected function migrate(mixed $migrationPayload) : Collection;
|
||||
|
||||
|
||||
/**
|
||||
* Pad a string to 8 chars min
|
||||
*/
|
||||
protected function padToValidBase32Secret(string $string)
|
||||
{
|
||||
return str_pad($string, 8, '=');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -47,8 +47,8 @@ class PlainTextMigrator extends Migrator
|
||||
$fakeAccount->id = -2;
|
||||
$fakeAccount->otp_type = substr($uri, 10, 4);
|
||||
// Only basic fields are filled to limit the risk of another exception.
|
||||
$fakeAccount->account = '## invalid OTP data ##';
|
||||
$fakeAccount->service = filter_input(INPUT_GET, 'issuer', FILTER_SANITIZE_ENCODED) ?? '-';
|
||||
$fakeAccount->account = __('twofaccounts.import.invalid_account');
|
||||
$fakeAccount->service = filter_input(INPUT_GET, 'issuer', FILTER_SANITIZE_ENCODED) ?? __('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();
|
||||
|
||||
|
121
app/Services/Migrators/TwoFASMigrator.php
Normal file
121
app/Services/Migrators/TwoFASMigrator.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Migrators;
|
||||
|
||||
use App\Services\Migrators\Migrator;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Models\TwoFAccount;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Exceptions\InvalidMigrationDataException;
|
||||
|
||||
class TwoFASMigrator extends Migrator
|
||||
{
|
||||
// {
|
||||
// "groups":
|
||||
// [
|
||||
// {
|
||||
// "id": "C2F69014-C4C7-4EEC-9225-D24E750F77FD",
|
||||
// "name": "Test",
|
||||
// "isExpanded": true
|
||||
// }
|
||||
// ],
|
||||
// "schemaVersion": 2,
|
||||
// "appOrigin": "ios",
|
||||
// "appVersionCode": 32001,
|
||||
// "services":
|
||||
// [
|
||||
// {
|
||||
// "secret": "NRTWO2DLNJUGO23KM5UA",
|
||||
// "badge":
|
||||
// {
|
||||
// "color": "Default"
|
||||
// },
|
||||
// "order":
|
||||
// {
|
||||
// "position": 0
|
||||
// },
|
||||
// "otp":
|
||||
// {
|
||||
// "account": "My account",
|
||||
// "digits": 6,
|
||||
// "counter": 0,
|
||||
// "period": 30,
|
||||
// "algorithm": "SHA1",
|
||||
// "tokenType": "TOTP"
|
||||
// },
|
||||
// "updatedAt": 1657529936000,
|
||||
// "type": "ManuallyAdded",
|
||||
// "name": "My Service",
|
||||
// "icon":
|
||||
// {
|
||||
// "selected": "Brand",
|
||||
// "brand":
|
||||
// {
|
||||
// "id": "ManuallyAdded"
|
||||
// },
|
||||
// "label":
|
||||
// {
|
||||
// "text": "OW",
|
||||
// "backgroundColor": "LightBlue"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ],
|
||||
// "appVersionName": "3.20.1"
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* Convert migration data to a TwoFAccounts collection.
|
||||
*
|
||||
* @param mixed $migrationPayload
|
||||
* @return \Illuminate\Support\Collection The converted accounts
|
||||
*/
|
||||
public function migrate(mixed $migrationPayload) : Collection
|
||||
{
|
||||
$json = json_decode(htmlspecialchars_decode($migrationPayload), true);
|
||||
|
||||
if (is_null($json) || Arr::has($json, 'services') == false) {
|
||||
Log::error('Aegis JSON migration data cannot be read');
|
||||
throw new InvalidMigrationDataException('2FAS Auth');
|
||||
}
|
||||
|
||||
foreach ($json['services'] as $key => $otp_parameters) {
|
||||
|
||||
$parameters = array();
|
||||
$parameters['otp_type'] = $otp_parameters['otp']['tokenType'];
|
||||
$parameters['service'] = $otp_parameters['name'];
|
||||
$parameters['account'] = $otp_parameters['otp']['account'] ?? $parameters['service'];
|
||||
$parameters['secret'] = $this->padToValidBase32Secret($otp_parameters['secret']);
|
||||
$parameters['algorithm'] = $otp_parameters['otp']['algorithm'];
|
||||
$parameters['digits'] = $otp_parameters['otp']['digits'];
|
||||
$parameters['counter'] = $otp_parameters['otp']['counter'] ?? null;
|
||||
$parameters['period'] = $otp_parameters['otp']['period'] ?? null;
|
||||
|
||||
try {
|
||||
$twofaccounts[$key] = new TwoFAccount;
|
||||
$twofaccounts[$key]->fillWithOtpParameters($parameters);
|
||||
}
|
||||
catch (\Exception $exception) {
|
||||
|
||||
Log::error(sprintf('Cannot instanciate a TwoFAccount object with 2FAS imported item #%s', $key));
|
||||
Log::error($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);
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@
|
||||
<!-- upload a file -->
|
||||
<div class="block">
|
||||
<label role="button" tabindex="0" class="button is-link is-rounded is-outlined" ref="fileInputLabel" @keyup.enter="$refs.fileInputLabel.click()">
|
||||
<input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="text/plain,application/json,text/csv" v-on:change="submitFile" ref="fileInput">
|
||||
<input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="text/plain,application/json,text/csv,.2fas" v-on:change="submitFile" ref="fileInput">
|
||||
{{ $t('twofaccounts.import.upload_a_file') }}
|
||||
</label>
|
||||
<field-error :form="uploadForm" field="file" />
|
||||
@ -54,6 +54,12 @@
|
||||
<span class="tag is-black">{{ $t('twofaccounts.import.plain_text') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-dark">2FAS Auth</span>
|
||||
<span class="tag is-black">JSON</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
@ -40,6 +40,7 @@ return [
|
||||
'auth_proxy_failed' => 'Proxy authentication failed',
|
||||
'auth_proxy_failed_legend' => '2Fauth is configured to run behind an authentication proxy but your proxy does not return the expected header. Check your configuration and try again.',
|
||||
'invalid_x_migration' => 'Invalid or unreadable :appname data',
|
||||
'invalid_2fa_data' => 'Invalid 2FA data',
|
||||
'unsupported_migration' => 'Data do not match any supported format',
|
||||
'unsupported_otp_type' => 'Unsupported OTP type',
|
||||
'encrypted_migration' => 'Unreadable, the data seem encrypted',
|
||||
|
@ -137,7 +137,7 @@ return [
|
||||
'import_legend' => '2FAuth can import data from various 2FA apps.<br />Use the Export feature of these apps to get a migration resource (a QR code or a file) and submit it using your preferred method below.',
|
||||
'upload_a_file' => 'Upload a file',
|
||||
'supported_formats_for_qrcode_upload' => 'Accepted: jpg, jpeg, png, bmp, gif, svg, or webp',
|
||||
'supported_formats_for_file_upload' => 'Accepted: Plain text, json, csv',
|
||||
'supported_formats_for_file_upload' => 'Accepted: Plain text, json, 2fas',
|
||||
'supported_migration_formats' => 'Supported migration formats',
|
||||
'qr_code' => 'QR code',
|
||||
'plain_text' => 'Plain text',
|
||||
@ -152,6 +152,8 @@ return [
|
||||
'discard_this_account' => 'Discard this account',
|
||||
'generate_a_test_password' => 'Generate a test pasword',
|
||||
'possible_duplicate' => 'An account with the exact same data already exists',
|
||||
'invalid_account' => '- invalid account -',
|
||||
'invalid_service' => '- invalid service -',
|
||||
],
|
||||
|
||||
];
|
Loading…
x
Reference in New Issue
Block a user