Преглед изворни кода

Add a user option to encrypt/decrypt sensitive db data

Bubka пре 4 година
родитељ
комит
53bb3b9c54

+ 113 - 1
app/Http/Controllers/Settings/OptionController.php

@@ -2,9 +2,15 @@
 
 namespace App\Http\Controllers\Settings;
 
+use Throwable;
+use App\TwoFAccount;
 use App\Classes\Options;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
 use App\Http\Controllers\Controller;
+use Illuminate\Support\Facades\Crypt;
+use Illuminate\Contracts\Encryption\EncryptException;
+use Illuminate\Contracts\Encryption\DecryptException;
 
 class OptionController extends Controller
 {
@@ -29,9 +35,115 @@ class OptionController extends Controller
      */
     public function store(Request $request)
     {
+        // The useEncryption option impacts the [existing] content of the database.
+        // Encryption/Decryption of the data is done only if the user change the value of the option
+        // to prevent successive encryption
+        
+        if( $request->useEncryption && !Options::get('useEncryption') ) {
+
+            // user enabled the encryption
+            if( !$this->encryptAccounts() ) {
+                return response()->json(['message' => __('errors.error_during_encryption'), 'settings' => Options::get()], 422);
+            }
+        }
+        else if( !$request->useEncryption && Options::get('useEncryption') ) {
+
+            // user disabled the encryption
+            if( !$this->decryptAccounts() ) {
+                return response()->json(['message' => __('errors.error_during_decryption'), 'settings' => Options::get()], 422);
+            }
+        }
+
         // Store all options
         Options::store($request->all());
 
-        return response()->json(['message' =>  __('settings.forms.setting_saved'), 'settings' => Options::get()], 200);
+        return response()->json(['message' => __('settings.forms.setting_saved'), 'settings' => Options::get()], 200);
+    }
+
+
+    /**
+     * Encrypt 2FA sensitive data
+     * @return boolean
+     */
+    private function encryptAccounts() : bool
+    {
+        // All existing records have to be encrypted without exception.
+        // This means that if any of the encryption failed we have to rollback
+        // all records to their original value.
+        
+        $twofaccounts = TwoFAccount::all();
+
+        $twofaccounts->each(function ($item, $key) {
+            try {
+                $item->uri = Crypt::encryptString($item->uri);
+                $item->account = Crypt::encryptString($item->account);
+            }
+            catch (EncryptException $e) {
+                return false;
+            }
+        });
+
+        return $this->tryUpdate($twofaccounts);
     }
+
+
+    /**
+     * Decrypt 2FA sensitive data
+     * @return boolean
+     */
+    private function decryptAccounts() : bool
+    {
+        // All existing records have to be decrypted without exception.
+        // This means that if any of the encryption failed we have to rollback
+        // all records to their original value.
+        
+        $twofaccounts = TwoFAccount::all();
+
+        $twofaccounts->each(function ($item, $key) {
+            try {
+                $item->uri = Crypt::decryptString($item->uri);
+                $item->account = Crypt::decryptString($item->account);
+            }
+            catch (DecryptException $e) {
+                return false;
+            }
+        });
+
+        return $this->tryUpdate($twofaccounts);
+    }
+
+
+    /**
+     * Try to update all records of the collection
+     * @param  Illuminate\Database\Eloquent\Collection $twofaccounts
+     * @return boolean                  
+     */
+    private function tryUpdate(\Illuminate\Database\Eloquent\Collection $twofaccounts) : bool
+    {
+        // The whole collection has its sensible data encrypted/decrypted, now we update the db
+        // using a transaction to ensure rollback if an exception is thrown
+
+        DB::beginTransaction();
+
+        try {
+            $twofaccounts->each(function ($item, $key) {
+                DB::table('twofaccounts')
+                    ->where('id', $item->id)
+                    ->update([
+                        'uri' => $item->uri,
+                        'account' => $item->account
+                    ]);
+            });
+
+            DB::commit();
+        }
+        catch (Throwable $e) {
+            DB::rollBack();
+
+            return false;
+        }
+
+        return true;
+    }
+
 }

+ 54 - 15
app/TwoFAccount.php

@@ -4,10 +4,12 @@ namespace App;
 
 use OTPHP\HOTP;
 use OTPHP\Factory;
+use App\Classes\Options;
 use Spatie\EloquentSortable\Sortable;
 use Spatie\EloquentSortable\SortableTrait;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Crypt;
 use Illuminate\Contracts\Encryption\DecryptException;
 
 class TwoFAccount extends Model implements Sortable
@@ -157,33 +159,70 @@ class TwoFAccount extends Model implements Sortable
 
 
     /**
-     * Set the user's first name.
+     * Set encrypted uri
      *
      * @param  string  $value
      * @return void
      */
-    // public function setUriAttribute($value)
-    // {
-    //     $this->attributes['uri'] = encrypt($value);
-    // }
+    public function setUriAttribute($value)
+    {
+        $this->attributes['uri'] = Options::get('useEncryption') ? Crypt::encryptString($value) : $value;
+    }
 
     /**
-     * Get the user's first name.
+     * Get decyphered uri
      *
      * @param  string  $value
      * @return string
      */
-    // public function getUriAttribute($value)
-    // {
-    //     try {
-
-    //         return decrypt($value);
+    public function getUriAttribute($value)
+    {
+        if( Options::get('useEncryption') )
+        {
+            try {
+                return Crypt::decryptString($value);
+            }
+            catch (DecryptException $e) {
+                return '*encrypted*';
+            }
+        }
+        else {
+            return $value;
+        }
+    }
 
-    //     } catch (DecryptException $e) {
 
-    //         return null;
-    //     }
+    /**
+     * Set encrypted account
+     *
+     * @param  string  $value
+     * @return void
+     */
+    public function setAccountAttribute($value)
+    {
+        $this->attributes['account'] = Options::get('useEncryption') ? Crypt::encryptString($value) : $value;
+    }
 
-    // }
+    /**
+     * Get decyphered account
+     *
+     * @param  string  $value
+     * @return string
+     */
+    public function getAccountAttribute($value)
+    {
+        if( Options::get('useEncryption') )
+        {
+            try {
+                return Crypt::decryptString($value);
+            }
+            catch (DecryptException $e) {
+                return '*encrypted*';
+            }
+        }
+        else {
+            return $value;
+        }
+    }
 
 }

+ 1 - 0
config/app.php

@@ -40,6 +40,7 @@ return [
         'showAccountsIcons' => true,
         'kickUserAfter' => '15',
         'activeGroup' => 0,
+        'useEncryption' => false,
     ],
 
     /*

+ 13 - 1
resources/js/views/settings/Options.vue

@@ -2,14 +2,25 @@
     <form-wrapper>
         <form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)">
             <h4 class="title is-4">{{ $t('settings.general') }}</h4>
+            <!-- Language -->
             <form-select :options="langs" :form="form" fieldName="lang" :label="$t('settings.forms.language.label')"  :help="$t('settings.forms.language.help')" />
+            <!-- display mode -->
             <form-select :options="layouts" :form="form" fieldName="displayMode" :label="$t('settings.forms.display_mode.label')" :help="$t('settings.forms.display_mode.help')" />
+            <!-- show icon -->
             <form-checkbox :form="form" fieldName="showAccountsIcons" :label="$t('settings.forms.show_accounts_icons.label')" :help="$t('settings.forms.show_accounts_icons.help')" />
+
             <h4 class="title is-4">{{ $t('settings.security') }}</h4>
+            <!-- auto lock -->
             <form-select :options="kickUserAfters" :form="form" fieldName="kickUserAfter" :label="$t('settings.forms.auto_lock.label')"  :help="$t('settings.forms.auto_lock.help')" />
+            <!-- protect db -->
+            <form-checkbox :form="form" fieldName="useEncryption" :label="$t('settings.forms.use_encryption.label')" :help="$t('settings.forms.use_encryption.help')" />
+            <!-- token as dot -->
             <form-checkbox :form="form" fieldName="showTokenAsDot" :label="$t('settings.forms.show_token_as_dot.label')" :help="$t('settings.forms.show_token_as_dot.help')" />
+            <!-- close token on copy -->
             <form-checkbox :form="form" fieldName="closeTokenOnCopy" :label="$t('settings.forms.close_token_on_copy.label')" :help="$t('settings.forms.close_token_on_copy.help')" />
+
             <h4 class="title is-4">{{ $t('settings.advanced') }}</h4>
+            <!-- basic qrcode -->
             <form-checkbox :form="form" fieldName="useBasicQrcodeReader" :label="$t('settings.forms.use_basic_qrcode_reader.label')" :help="$t('settings.forms.use_basic_qrcode_reader.help')" />
         </form>
     </form-wrapper>
@@ -30,6 +41,7 @@
                     showAccountsIcons: this.$root.appSettings.showAccountsIcons,
                     displayMode: this.$root.appSettings.displayMode,
                     kickUserAfter: this.$root.appSettings.kickUserAfter,
+                    useEncryption: this.$root.appSettings.useEncryption,
                 }),
                 langs: [
                     { text: this.$t('languages.en'), value: 'en' },
@@ -73,7 +85,7 @@
                     
                     this.$notify({ type: 'is-danger', text: error.response.data.message })
                 });
-            }
+            },
         },
     }
 </script>

+ 3 - 1
resources/lang/en/errors.php

@@ -23,6 +23,8 @@ return [
     ],
     'something_wrong_with_server' => 'Something is wrong with your server',
     'Unable_to_decrypt_uri' => 'Unable to decrypt uri',
-    'wrong_current_password' => 'Wrong current password, nothing has changed'
+    'wrong_current_password' => 'Wrong current password, nothing has changed',
+    'error_during_encryption' => 'Encryption failed, your database remains unprotected',
+    'error_during_decryption' => 'Decryption failed, your database is still protected',
 
 ];

+ 4 - 1
resources/lang/en/settings.php

@@ -56,6 +56,10 @@ return [
             'label' => 'Auto lock',
             'help' => 'Log out the user automatically in case of inactivity'
         ],
+        'use_encryption' => [
+            'label' => 'Protect sensible data',
+            'help' => 'Sensitive data, the 2FA secrets and emails, are stored encrypted in database. Be sure to backup the APP_KEY value of your .env file (or the whole file) as it serves as key encryption. There is no way to decypher encrypted data without this key.',
+        ],
         'never' => 'Never',
         'on_token_copy' => 'On security code copy',
         '1_minutes' => 'After 1 minute',
@@ -66,6 +70,5 @@ return [
         '1_hour' => 'After 1 hour',
         '1_day' => 'After 1 day',
     ],
-    
 
 ];