浏览代码

Move G-Auth import logic from controller to service

Bubka 3 年之前
父节点
当前提交
c20e5f79ef

+ 4 - 43
app/Api/v1/Controllers/TwoFAccountController.php

@@ -129,50 +129,11 @@ class TwoFAccountController extends Controller
      * @return \App\Api\v1\Resources\TwoFAccountCollection
      */
     public function import(TwoFAccountImportRequest $request)
-    {
-        $ALGORITHM = [
-            '',
-            'sha1',
-            'sha256',
-            'sha512',
-            'md5'
-        ];
-
-        $DIGIT_COUNT = [
-            '',
-            6,
-            8
-        ];
-
-        $OTP_TYPE = [
-            '',
-            'hotp',
-            'totp'
-        ];
-
-        // require_once base_path('protobuf/SearchRequest.php');
-        // $uri = 'otpauth-migration://offline?data=CjUKCi8gSXtDdoRpZEkSEWVkb3VhcmRAZ2FuZWF1Lm1lGg5iYW5rLmdhbmVhdS5tZSABKAEwAhABGAEgAA==';
-        // $uri = base64_decode(urldecode('CjUKCi8gSXtDdoRpZEkSEWVkb3VhcmRAZ2FuZWF1Lm1lGg5iYW5rLmdhbmVhdS5tZSABKAEwAhABGAEgAA=='));
-        // $uri = 'otpauth-migration://offline?data=CiQKCj1PS8k1EUgVI0ESB0BidWJrYV8aB1R3aXR0ZXIgASgBMAIKIQoK6/l62ezmsWvMNRIFQnVia2EaBkdpdEh1YiABKAEwAhABGAEgAA==';
-        // $uri = base64_decode(urldecode('CiQKCj1PS8k1EUgVI0ESB0BidWJrYV8aB1R3aXR0ZXIgASgBMAIKIQoK6/l62ezmsWvMNRIFQnVia2EaBkdpdEh1YiABKAEwAhABGAEgAA=='));
-
-        $data = base64_decode(urldecode(Str::replace('otpauth-migration://offline?data=', '', $request->uri)));
-
-        $proto = new \App\Protobuf\GoogleAuth\Payload();
-        $proto->mergeFromString($data);
-        $otpParameters = $proto->getOtpParameters();
-
-        foreach ($otpParameters->getIterator() as $key => $otp_parameters) {
-            $out[$key]['secret'] = \ParagonIE\ConstantTime\Base32::encodeUpper($otp_parameters->getSecret());
-            $out[$key]['account'] = $otp_parameters->getName();
-            $out[$key]['service'] = $otp_parameters->getIssuer();
-            $out[$key]['algorithm'] = $ALGORITHM[$otp_parameters->getalgorithm()];
-            $out[$key]['digits'] = $DIGIT_COUNT[$otp_parameters->getDigits()];
-            $out[$key]['otp_type'] = $OTP_TYPE[$otp_parameters->getType()];
-            $out[$key]['counter'] = $otp_parameters->getCounter();
-        }
+    { 
+        $request->merge(['withSecret' => true]);
+        $twofaccounts = $this->twofaccountService->convertMigrationFromGA($request->uri);
 
-        return response()->json($out, 200);
+        return new TwoFAccountCollection($twofaccounts);
     }
 
 

+ 5 - 0
app/Exceptions/Handler.php

@@ -60,6 +60,11 @@ class Handler extends ExceptionHandler
                 'message' => $exception->getMessage()], 400);
         });
 
+        $this->renderable(function (InvalidGoogleAuthMigration $exception, $request) {
+            return response()->json([
+                'message' => __('errors.invalid_google_auth_migration')], 400);
+        });
+
         $this->renderable(function (\Illuminate\Auth\AuthenticationException $exception, $request) {
             if ($exception->guards() === ['reverse-proxy-guard']) {
                 return response()->json([

+ 14 - 0
app/Exceptions/InvalidGoogleAuthMigration.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Exceptions;
+
+use Exception;
+
+/**
+ * Class UndecipherableException.
+ *
+ * @codeCoverageIgnore
+ */
+class InvalidGoogleAuthMigration extends Exception
+{
+}

+ 30 - 0
app/Protobuf/GAuthValueMapping.php

@@ -0,0 +1,30 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: GoogleAuth.proto
+
+namespace App\Protobuf;
+
+class GAuthValueMapping
+{
+    const ALGORITHM = [
+        'ALGORITHM_UNSPECIFIED' => '',
+        'ALGORITHM_SHA1' => 'sha1',
+        'ALGORITHM_SHA256' => 'sha256',
+        'ALGORITHM_SHA512' => 'sha512',
+        'ALGORITHM_MD5' => 'md5'
+    ];
+
+    const DIGIT_COUNT = [
+        'DIGIT_COUNT_UNSPECIFIED' => '',
+        'DIGIT_COUNT_SIX' => 6,
+        'DIGIT_COUNT_EIGHT' => 8
+    ];
+
+    const OTP_TYPE = [
+        'OTP_TYPE_UNSPECIFIED' => '',
+        'OTP_TYPE_HOTP' => 'hotp',
+        'OTP_TYPE_TOTP' => 'totp'
+    ];
+
+    private function __construct() {}
+}

+ 7 - 0
app/Protobuf/README.md

@@ -0,0 +1,7 @@
+# Protobuf class generation
+
+## cli
+
+```sh
+protoc --proto_path=app/Protobuf/ --php_out=. app/Protobuf/GoogleAuth.proto
+```

+ 98 - 0
app/Services/TwoFAccountService.php

@@ -6,16 +6,25 @@ use App\Models\TwoFAccount;
 use App\Exceptions\InvalidSecretException;
 use App\Exceptions\InvalidOtpParameterException;
 use App\Exceptions\UndecipherableException;
+use App\Exceptions\InvalidGoogleAuthMigration;
 use App\Services\Dto\OtpDto;
 use App\Services\Dto\TwoFAccountDto;
+use Exception;
 use OTPHP\TOTP;
 use OTPHP\HOTP;
 use OTPHP\Factory;
 use Illuminate\Support\Str;
 use Illuminate\Support\Arr;
+use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Validation\ValidationException;
+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
 {
@@ -228,6 +237,66 @@ 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 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();
+        }
+
+        foreach ($otpParameters->getIterator() as $key => $otp_parameters) {
+
+             try {
+                $parameters['otp_type']     = GAuthValueMapping::OTP_TYPE[OtpType::name($otp_parameters->getType())];
+                $parameters['account']      = $otp_parameters->getName();
+                $parameters['service']      = $otp_parameters->getIssuer();
+                $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']      = $otp_parameters->getCounter();
+                // $parameters['period']       = $otp_parameters->getPeriod();
+
+                $twofaccounts[$key] = $this->createFromParameters($parameters, false);
+             }
+             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  = '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 $this->markAsDuplicate(collect($twofaccounts));
+
+    }
+
+
 // ########################################################################################################################
 // ########################################################################################################################
 // ########################################################################################################################
@@ -473,4 +542,33 @@ class TwoFAccountService
         }
         // @codeCoverageIgnoreEnd
     }
+
+
+    /**
+     * Return the given collection with items marked as Duplicates (using id=-1) if a similar record exists in database
+     * 
+     * @param \Illuminate\Support\Collection
+     * @return \Illuminate\Support\Collection
+     */
+    private function markAsDuplicate($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;
+    }
 }