Add a user option to encrypt/decrypt sensitive db data

This commit is contained in:
Bubka 2020-10-31 01:16:15 +01:00
parent fe02bac6d6
commit 53bb3b9c54
6 changed files with 188 additions and 19 deletions

View File

@ -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 @@ public function index()
*/
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;
}
}

View File

@ -4,10 +4,12 @@
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 @@ public function getCounterAttribute()
/**
* 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 {
public function getUriAttribute($value)
{
if( Options::get('useEncryption') )
{
try {
return Crypt::decryptString($value);
}
catch (DecryptException $e) {
return '*encrypted*';
}
}
else {
return $value;
}
}
// return decrypt($value);
// } catch (DecryptException $e) {
/**
* Set encrypted account
*
* @param string $value
* @return void
*/
public function setAccountAttribute($value)
{
$this->attributes['account'] = Options::get('useEncryption') ? Crypt::encryptString($value) : $value;
}
// return null;
// }
// }
/**
* 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;
}
}
}

View File

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

View File

@ -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>

View File

@ -23,6 +23,8 @@
],
'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',
];

View File

@ -56,6 +56,10 @@
'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 @@
'1_hour' => 'After 1 hour',
'1_day' => 'After 1 day',
],
];