浏览代码

Refactor Import feature to support more sources of export

Bubka 2 年之前
父节点
当前提交
41387453d5

+ 55 - 0
app/Api/v1/Controllers/ImportController.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Api\v1\Controllers;
+
+use App\Api\v1\Requests\TwoFAccountImportRequest;
+use App\Api\v1\Resources\TwoFAccountCollection;
+use App\Contracts\MigrationService;
+use App\Http\Controllers\Controller;
+
+class ImportController extends Controller
+{
+    /**
+     * @var $migrator The Migration service
+     */
+    protected $migrator;
+
+
+    /**
+     * Constructor
+     */
+    public function __construct(MigrationService $migrationService)
+    {
+        $this->migrator = $migrationService;
+    }
+
+
+    /**
+     * Convert Google Auth data to a TwoFAccounts collection
+     *
+     * @param  \App\Api\v1\Requests\TwoFAccountImportRequest  $request
+     * @return \App\Api\v1\Resources\TwoFAccountCollection
+     */
+    public function googleAuth(TwoFAccountImportRequest $request)
+    { 
+        $request->merge(['withSecret' => true]);
+        $twofaccounts = $this->migrator->migrate($request->uri);
+
+        return new TwoFAccountCollection($twofaccounts);
+    }
+
+
+    /**
+     * Convert Aegis data to a TwoFAccounts collection
+     *
+     * @param  \App\Api\v1\Requests\TwoFAccountImportRequest  $request
+     * @return \App\Api\v1\Resources\TwoFAccountCollection
+     */
+    public function aegis(TwoFAccountImportRequest $request)
+    { 
+        $request->merge(['withSecret' => true]);
+        $twofaccounts = $this->migrator->migrate($request->uri);
+
+        return new TwoFAccountCollection($twofaccounts);
+    }
+}

+ 0 - 15
app/Api/v1/Controllers/TwoFAccountController.php

@@ -102,21 +102,6 @@ class TwoFAccountController extends Controller
     }
 
 
-    /**
-     * Dry-import Google authenticator data
-     *
-     * @param  \App\Api\v1\Requests\TwoFAccountImportRequest  $request
-     * @return \App\Api\v1\Resources\TwoFAccountCollection
-     */
-    public function import(TwoFAccountImportRequest $request)
-    { 
-        $request->merge(['withSecret' => true]);
-        $twofaccounts = TwoFAccounts::convertMigrationFromGA($request->uri);
-
-        return new TwoFAccountCollection($twofaccounts);
-    }
-
-
     /**
      * Save 2FA accounts order
      *

+ 16 - 0
app/Contracts/MigrationService.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Contracts;
+
+use \Illuminate\Support\Collection;
+
+interface MigrationService
+{
+    /**
+     * Convert migration data to a 2FAccounts collection.
+     *
+     * @param  mixed  $migrationPayload
+     * @return \Illuminate\Support\Collection The converted accounts
+     */
+    public function migrate(mixed $migrationPayload) : Collection;
+}

+ 62 - 0
app/Providers/MigrationServiceProvider.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Providers;
+
+use App\Api\v1\Controllers\ImportController;
+use App\Contracts\MigrationService;
+use App\Services\Migrators\GoogleAuthMigrator;
+use App\Services\Migrators\AegisMigrator;
+use App\Services\Migrators\PlainTextMigrator;
+use Illuminate\Support\ServiceProvider;
+use Illuminate\Contracts\Support\DeferrableProvider;
+
+class MigrationServiceProvider extends ServiceProvider implements DeferrableProvider
+{
+    /**
+     * Register services.
+     *
+     * @return void
+     */
+    public function register()
+    {            
+        $this->app->when(ImportController::class)
+            ->needs(MigrationService::class)
+            ->give(function () {
+                switch (request()->route()->getName()) {
+                    case 'import.googleAuth':
+                        return $this->app->get(GoogleAuthMigrator::class);
+
+                    case 'import.aegis':
+                        return $this->app->get(AegisMigrator::class);
+                    
+                    default:
+                        return $this->app->get(PlainTextMigrator::class);
+                }
+            });
+    }
+
+    /**
+     * Bootstrap services.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        //
+    }
+
+
+    /**
+     * Get the services provided by the provider.
+     *
+     * @return array
+     */
+    public function provides()
+    {
+        return [
+            GoogleAuthMigrator::class,
+            AegisMigrator::class,
+            PlainTextMigrator::class,
+        ];
+    }
+}

+ 20 - 0
app/Services/Migrators/AegisMigrator.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Services\Migrators;
+
+use App\Contracts\MigrationService;
+use \Illuminate\Support\Collection;
+
+class AegisMigrator implements MigrationService
+{
+    /**
+     * Convert migration data to a 2FAccounts collection.
+     *
+     * @param  mixed  $migrationPayload
+     * @return \Illuminate\Support\Collection The converted accounts
+     */
+    public function migrate(mixed $migrationPayload) : Collection
+    {
+        return Collect(['collected from aegisMigrator']);
+    }
+}

+ 83 - 0
app/Services/Migrators/GoogleAuthMigrator.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace App\Services\Migrators;
+
+use Exception;
+use App\Models\TwoFAccount;
+use App\Contracts\MigrationService;
+use \Illuminate\Support\Collection;
+use ParagonIE\ConstantTime\Base32;
+use App\Protobuf\GAuthValueMapping;
+use App\Protobuf\GoogleAuth\Payload;
+use App\Protobuf\GoogleAuth\Payload\OtpType;
+use App\Protobuf\GoogleAuth\Payload\Algorithm;
+use App\Protobuf\GoogleAuth\Payload\DigitCount;
+use App\Exceptions\InvalidGoogleAuthMigration;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Str;
+
+class GoogleAuthMigrator extends Migrator implements MigrationService
+{
+
+    /**
+     * Convert Google Authenticator migration URI to a set of TwoFAccount objects.
+     *
+     * @param  mixed  $migrationPayload migration uri provided by Google Authenticator export feature
+     * 
+     * @return \Illuminate\Support\Collection The converted accounts
+     */
+    public function migrate(mixed $migrationPayload) : Collection
+    {
+        try {
+            $migrationData = base64_decode(urldecode(Str::replace('otpauth-migration://offline?data=', '', $migrationPayload)));
+            $protobuf = new Payload();
+            $protobuf->mergeFromString($migrationData);
+            $otpParameters = $protobuf->getOtpParameters();
+        }
+        catch (Exception $ex) {
+            Log::error("Protobuf failed to get OTP parameters from provided migration URI");
+            Log::error($ex->getMessage());
+
+            throw new InvalidGoogleAuthMigration();
+        }
+
+        $twofaccounts = array();
+        
+        foreach ($otpParameters->getIterator() as $key => $otp_parameters) {
+
+             try {
+                $parameters = array();
+                $parameters['otp_type']     = GAuthValueMapping::OTP_TYPE[OtpType::name($otp_parameters->getType())];
+                $parameters['service']      = $otp_parameters->getIssuer();
+                $parameters['account']      = str_replace($parameters['service'].':', '', $otp_parameters->getName());
+                $parameters['secret']       = Base32::encodeUpper($otp_parameters->getSecret());
+                $parameters['algorithm']    = GAuthValueMapping::ALGORITHM[Algorithm::name($otp_parameters->getAlgorithm())];
+                $parameters['digits']       = GAuthValueMapping::DIGIT_COUNT[DigitCount::name($otp_parameters->getDigits())];
+                $parameters['counter']      = $parameters['otp_type'] === TwoFAccount::HOTP ? $otp_parameters->getCounter() : null;
+                $parameters['period']       = $parameters['otp_type'] === TwoFAccount::TOTP ? $otp_parameters->getPeriod() : null;
+
+                $twofaccounts[$key] = new TwoFAccount;
+                $twofaccounts[$key]->fillWithOtpParameters($parameters);
+            }
+            catch (Exception $exception) {
+
+                Log::error(sprintf('Cannot instanciate a TwoFAccount object with OTP parameters from 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 = -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();
+                // 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 self::markAsDuplicate(collect($twofaccounts));
+    }
+}

+ 39 - 0
app/Services/Migrators/Migrator.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Services\Migrators;
+
+use App\Models\TwoFAccount;
+use \Illuminate\Support\Collection;
+
+abstract class Migrator
+{
+
+    /**
+     * Return the given collection with items marked as Duplicates (using id=-1) if a similar record exists in database
+     * 
+     * @param \Illuminate\Support\Collection $twofaccounts
+     * @return \Illuminate\Support\Collection
+     */
+    protected static function markAsDuplicate(Collection $twofaccounts) : Collection
+    {
+        $storage = TwoFAccount::all();
+
+        $twofaccounts = $twofaccounts->map(function ($twofaccount, $key) use ($storage) {
+            if ($storage->contains(function ($value, $key) use ($twofaccount) {
+                return $value->secret == $twofaccount->secret
+                    && $value->service == $twofaccount->service
+                    && $value->account == $twofaccount->account
+                    && $value->otp_type == $twofaccount->otp_type
+                    && $value->digits == $twofaccount->digits
+                    && $value->algorithm == $twofaccount->algorithm;
+            })) {
+                $twofaccount->id = -1;
+            }
+
+            return $twofaccount;
+        });
+
+        return $twofaccounts;
+    }
+
+}

+ 20 - 0
app/Services/Migrators/PlainTextMigrator.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Services\Migrators;
+
+use App\Contracts\MigrationService;
+use \Illuminate\Support\Collection;
+
+class PlainTextMigrator implements MigrationService
+{
+    /**
+     * Convert migration data to a 2FAccounts collection.
+     *
+     * @param  mixed  $migrationPayload
+     * @return \Illuminate\Support\Collection The converted accounts
+     */
+    public function migrate(mixed $migrationPayload) : Collection
+    {
+        return Collect(['collected from plainTextMigrator']);
+    }
+}

+ 0 - 103
app/Services/TwoFAccountService.php

@@ -3,17 +3,7 @@
 namespace App\Services;
 
 use App\Models\TwoFAccount;
-use App\Exceptions\InvalidGoogleAuthMigration;
-use Exception;
-use Illuminate\Support\Str;
-use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Log;
-use ParagonIE\ConstantTime\Base32;
-use App\Protobuf\GAuthValueMapping;
-use App\Protobuf\GoogleAuth\Payload;
-use App\Protobuf\GoogleAuth\Payload\OtpType;
-use App\Protobuf\GoogleAuth\Payload\Algorithm;
-use App\Protobuf\GoogleAuth\Payload\DigitCount;
 
 class TwoFAccountService
 {
@@ -60,70 +50,6 @@ class TwoFAccountService
     }
 
 
-    /**
-     * Convert Google Authenticator migration URI to a set of TwoFAccount objects
-     * 
-     * @param string $migrationUri migration uri provided by Google Authenticator export feature
-     * 
-     * @return \Illuminate\Support\Collection The converted accounts
-     */
-    public static function convertMigrationFromGA($migrationUri) : Collection
-    {
-        try {
-            $migrationData = base64_decode(urldecode(Str::replace('otpauth-migration://offline?data=', '', $migrationUri)));
-            $protobuf = new Payload();
-            $protobuf->mergeFromString($migrationData);
-            $otpParameters = $protobuf->getOtpParameters();
-        }
-        catch (Exception $ex) {
-            Log::error("Protobuf failed to get OTP parameters from provided migration URI");
-            Log::error($ex->getMessage());
-
-            throw new InvalidGoogleAuthMigration();
-        }
-
-        $twofaccounts = array();
-        
-        foreach ($otpParameters->getIterator() as $key => $otp_parameters) {
-
-             try {
-                $parameters = array();
-                $parameters['otp_type']     = GAuthValueMapping::OTP_TYPE[OtpType::name($otp_parameters->getType())];
-                $parameters['service']      = $otp_parameters->getIssuer();
-                $parameters['account']      = str_replace($parameters['service'].':', '', $otp_parameters->getName());
-                $parameters['secret']       = Base32::encodeUpper($otp_parameters->getSecret());
-                $parameters['algorithm']    = GAuthValueMapping::ALGORITHM[Algorithm::name($otp_parameters->getAlgorithm())];
-                $parameters['digits']       = GAuthValueMapping::DIGIT_COUNT[DigitCount::name($otp_parameters->getDigits())];
-                $parameters['counter']      = $parameters['otp_type'] === TwoFAccount::HOTP ? $otp_parameters->getCounter() : null;
-                $parameters['period']       = $parameters['otp_type'] === TwoFAccount::TOTP ? $otp_parameters->getPeriod() : null;
-
-                $twofaccounts[$key] = new TwoFAccount;
-                $twofaccounts[$key]->fillWithOtpParameters($parameters);
-            }
-            catch (Exception $exception) {
-
-                Log::error(sprintf('Cannot instanciate a TwoFAccount object with OTP parameters from 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 = -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();
-                // 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 self::markAsDuplicate(collect($twofaccounts));
-
-    }
-
-
     /**
      * Explode a comma separated list of IDs to an array of IDs
      * 
@@ -141,33 +67,4 @@ class TwoFAccountService
         
         return $ids;
     }
-
-
-    /**
-     * Return the given collection with items marked as Duplicates (using id=-1) if a similar record exists in database
-     * 
-     * @param \Illuminate\Support\Collection $twofaccounts
-     * @return \Illuminate\Support\Collection
-     */
-    private static function markAsDuplicate(Collection $twofaccounts) : Collection
-    {
-        $storage = TwoFAccount::all();
-
-        $twofaccounts = $twofaccounts->map(function ($twofaccount, $key) use ($storage) {
-            if ($storage->contains(function ($value, $key) use ($twofaccount) {
-                return $value->secret == $twofaccount->secret
-                    && $value->service == $twofaccount->service
-                    && $value->account == $twofaccount->account
-                    && $value->otp_type == $twofaccount->otp_type
-                    && $value->digits == $twofaccount->digits
-                    && $value->algorithm == $twofaccount->algorithm;
-            })) {
-                $twofaccount->id = -1;
-            }
-
-            return $twofaccount;
-        });
-
-        return $twofaccounts;
-    }
 }

+ 1 - 0
config/app.php

@@ -175,6 +175,7 @@ return [
         App\Providers\EventServiceProvider::class,
         App\Providers\RouteServiceProvider::class,
         App\Providers\TwoFAuthServiceProvider::class,
+        App\Providers\MigrationServiceProvider::class,
 
     ],
 

+ 2 - 2
resources/js/views/twofaccounts/Import.vue

@@ -145,8 +145,8 @@
             if( this.$route.params.migrationUri ) {
                 this.migrationUri = this.$route.params.migrationUri
                 this.isFetching = true
-
-                await this.axios.post('/api/v1/twofaccounts/import', { uri: this.migrationUri }).then(response => {
+                
+                await this.axios.post('/api/v1/import/google-auth', { uri: this.migrationUri }).then(response => {
                     response.data.forEach((data) => {
                         data.imported = -1;
                         this.exportedAccounts.push(data)

+ 3 - 1
routes/api/v1.php

@@ -27,7 +27,6 @@ Route::group(['middleware' => 'auth:api-guard'], function () {
     Route::delete('settings/{settingName}', 'SettingController@destroy')->name('settings.destroy');
 
     Route::delete('twofaccounts', 'TwoFAccountController@batchDestroy')->name('twofaccounts.batchDestroy');
-    Route::post('twofaccounts/import', 'TwoFAccountController@import')->name('twofaccounts.import');
     Route::patch('twofaccounts/withdraw', 'TwoFAccountController@withdraw')->name('twofaccounts.withdraw');
     Route::post('twofaccounts/reorder', 'TwoFAccountController@reorder')->name('twofaccounts.reorder');
     Route::post('twofaccounts/preview', 'TwoFAccountController@preview')->name('twofaccounts.preview');
@@ -37,6 +36,9 @@ Route::group(['middleware' => 'auth:api-guard'], function () {
     Route::post('twofaccounts/otp', 'TwoFAccountController@otp')->name('twofaccounts.otp');
     Route::apiResource('twofaccounts', 'TwoFAccountController');
 
+    Route::post('import/google-auth', 'ImportController@googleAuth')->name('import.googleAuth');
+    Route::post('import/aegis', 'ImportController@aegis')->name('import.aegis');
+
     Route::get('groups/{group}/twofaccounts', 'GroupController@accounts')->name('groups.show.twofaccounts');
     Route::post('groups/{group}/assign', 'GroupController@assignAccounts')->name('groups.assign.twofaccounts');
     Route::apiResource('groups', 'GroupController');