Skip to content

Commit

Permalink
Add option to encrypt shared secret and recovery codes
Browse files Browse the repository at this point in the history
  • Loading branch information
srichter committed Apr 25, 2021
1 parent 36ae690 commit a16eca3
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 4 deletions.
7 changes: 5 additions & 2 deletions config/laraguard.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,10 @@
|--------------------------------------------------------------------------
|
| While this package uses recommended RFC 4226 and RDC 6238 settings, you
| can further configure how TOTP should work. These settings are saved
| for each 2FA authentication, so it will only affect new accounts.
| can further configure how TOTP should work. You can specify whether the
| shared secret and recovery codes should be stored in the db encrypted.
| These settings are saved for each 2FA authentication, so it will only
| affect new accounts.
|
*/

Expand All @@ -139,6 +141,7 @@
'seconds' => 30,
'window' => 1,
'algorithm' => 'sha1',
// 'encrypted' => true,
],

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public function up()
$table->unsignedTinyInteger('seconds')->default(30);
$table->unsignedTinyInteger('window')->default(0);
$table->string('algorithm', 16)->default('sha1');
$table->boolean('encrypted')->default(false);
$table->json('recovery_codes')->nullable();
$table->timestampTz('recovery_codes_generated_at')->nullable();
$table->json('safe_devices')->nullable();
Expand Down
9 changes: 8 additions & 1 deletion src/Eloquent/HandlesCodes.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use DateTime;
use Illuminate\Support\Carbon;
use ParagonIE\ConstantTime\Base32;
use Illuminate\Support\Facades\Crypt;

trait HandlesCodes
{
Expand Down Expand Up @@ -142,7 +143,13 @@ protected function timestampToBinary(int $timestamp)
*/
protected function getBinarySecret()
{
return Base32::decodeUpper($this->attributes['shared_secret']);
$secret = $this->attributes['shared_secret'];

if ($this->encrypted) {
$secret = Crypt::decryptString($secret);
}

return Base32::decodeUpper($secret);
}

/**
Expand Down
33 changes: 33 additions & 0 deletions src/Eloquent/HandlesRecoveryCodes.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\Str;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Crypt;

trait HandlesRecoveryCodes
{
Expand Down Expand Up @@ -69,4 +70,36 @@ public static function generateRecoveryCodes(int $amount, int $length)
];
});
}

/**
* Decrypts the Recovery Codes attribute.
*
* @param $value
* @return null|\Illuminate\Support\Collection
*/
public static function decryptRecoveryCodes($value)
{
return optional($value)->map(function ($item) {
return [
'code' => Crypt::decryptString($item['code']),
'used_at' => $item['used_at'],
];
});
}

/**
* Encrypts the Recovery Codes attribute.
*
* @param $value
* @return null|\Illuminate\Support\Collection
*/
public static function encryptRecoveryCodes($value)
{
return optional($value)->map(function ($item) {
return [
'code' => Crypt::encryptString($item['code']),
'used_at' => $item['used_at'],
];
});
}
}
76 changes: 75 additions & 1 deletion src/Eloquent/TwoFactorAuthentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Database\Eloquent\Factories\HasFactory;
use ParagonIE\ConstantTime\Base32;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Database\Eloquent\Model;
use DarkGhostHunter\Laraguard\Contracts\TwoFactorTotp;

Expand All @@ -20,6 +21,7 @@
* @property int $seconds
* @property int $window
* @property string $algorithm
* @property bool $encrypted
* @property array $totp_config
* @property null|\Illuminate\Support\Collection $recovery_codes
* @property null|\Illuminate\Support\Collection $safe_devices
Expand Down Expand Up @@ -47,6 +49,7 @@ class TwoFactorAuthentication extends Model implements TwoFactorTotp
'digits' => 'int',
'seconds' => 'int',
'window' => 'int',
'encrypted' => 'bool',
'recovery_codes' => 'collection',
'safe_devices' => 'collection',
];
Expand All @@ -71,6 +74,41 @@ public function authenticatable()
return $this->morphTo('authenticatable');
}

/**
* Gets the Shared Secret attribute from its binary form.
*
* @param $value
* @return null|string
*/
protected function getSharedSecretAttribute($value)
{
if ($value === null) {
return $value;
}

if ($this->encrypted) {
$value = Crypt::decryptString($value);
}

return $value;
}

/**
* Sets the Shared Secret attribute to its binary form.
*
* @param $value
* @return $this
*/
protected function setSharedSecretAttribute($value)
{
if ($this->encrypted) {
$value = Crypt::encryptString($value);
}

$this->attributes['shared_secret'] = $value;

return $this;
}

/**
* Sets the Algorithm to lowercase.
Expand All @@ -82,6 +120,42 @@ protected function setAlgorithmAttribute($value)
$this->attributes['algorithm'] = strtolower($value);
}

/**
* Gets the Recovery Codes attribute, optionally from its encrypted form.
*
* @param $value
* @return null|\Illuminate\Support\Collection
*/
protected function getRecoveryCodesAttribute($value)
{
$value = $this->castAttribute('recovery_codes', $value);

if ($this->encrypted) {
$value = static::decryptRecoveryCodes($value);
}

return $value;
}

/**
* Sets the Recovery Codes attribute, optionally to its encrypted form.
*
* @param $value
* @return $this
*/
protected function setRecoveryCodesAttribute($value)
{
if ($this->encrypted) {
$value = static::encryptRecoveryCodes($value);
}

$value = $this->castAttributeAsJson('recovery_codes', $value);

$this->attributes['recovery_codes'] = $value;

return $this;
}

/**
* Returns if the Two Factor Authentication has been enabled.
*
Expand Down Expand Up @@ -116,7 +190,7 @@ public function flushAuth()

$this->attributes = array_merge($this->attributes, config('laraguard.totp'));

$this->attributes['shared_secret'] = static::generateRandomSecret();
$this->setSharedSecretAttribute(static::generateRandomSecret());

return $this;
}
Expand Down
119 changes: 119 additions & 0 deletions tests/Eloquent/TwoFactorAuthenticationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Tests\Stubs\UserTwoFactorStub;
use Tests\RunsPublishableMigrations;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use DarkGhostHunter\Laraguard\Eloquent\TwoFactorAuthentication;
Expand Down Expand Up @@ -45,6 +46,124 @@ public function test_returns_authenticatable()
}


public function test_encrypts_shared_secret_if_enabled()
{
$tfa = new TwoFactorAuthentication();

$tfa->encrypted = true;

$tfa->shared_secret = 'CCSWDTKINR7YM3TR5LQ6EEVIYY5PZRZ2';

$attributes = $tfa->getAttributes();

$this->assertEquals('CCSWDTKINR7YM3TR5LQ6EEVIYY5PZRZ2', Crypt::decryptString($attributes['shared_secret']));
}

public function test_does_not_encrypt_shared_secret_if_disabled()
{
$tfa = new TwoFactorAuthentication();

$tfa->encrypted = false;

$tfa->shared_secret = 'CCSWDTKINR7YM3TR5LQ6EEVIYY5PZRZ2';

$attributes = $tfa->getAttributes();

$this->assertEquals('CCSWDTKINR7YM3TR5LQ6EEVIYY5PZRZ2', $attributes['shared_secret']);
}

public function test_decrypts_shared_secret_if_enabled()
{
$tfa = new TwoFactorAuthentication();

$tfa->encrypted = true;

$attributes = $tfa->getAttributes();
$attributes['shared_secret'] = Crypt::encryptString('CCSWDTKINR7YM3TR5LQ6EEVIYY5PZRZ2');
$tfa->setRawAttributes($attributes);

$this->assertEquals('CCSWDTKINR7YM3TR5LQ6EEVIYY5PZRZ2', $tfa->shared_secret);
}

public function test_does_not_decrypt_shared_secret_if_disabled()
{
$tfa = new TwoFactorAuthentication();

$tfa->encrypted = false;

$attributes = $tfa->getAttributes();
$attributes['shared_secret'] = 'CCSWDTKINR7YM3TR5LQ6EEVIYY5PZRZ2';
$tfa->setRawAttributes($attributes);

$this->assertEquals('CCSWDTKINR7YM3TR5LQ6EEVIYY5PZRZ2', $tfa->shared_secret);
}

public function test_encrypts_recovery_codes_if_enabled()
{
$tfa = new TwoFactorAuthentication();

$tfa->encrypted = true;

$recoveryCodes = TwoFactorAuthentication::generateRecoveryCodes(1, 7);

$tfa->recovery_codes = $recoveryCodes;

$attributes = $tfa->getAttributes();

$rawCodes = json_decode($attributes['recovery_codes'], true);

$this->assertEquals($recoveryCodes->first()['code'], Crypt::decryptString($rawCodes[0]['code']));
}

public function test_does_not_encrypt_recovery_codes_if_disabled()
{
$tfa = new TwoFactorAuthentication();

$tfa->encrypted = false;

$recoveryCodes = TwoFactorAuthentication::generateRecoveryCodes(1, 7);

$tfa->recovery_codes = $recoveryCodes;

$attributes = $tfa->getAttributes();

$rawCodes = json_decode($attributes['recovery_codes'], true);

$this->assertEquals($recoveryCodes->first()['code'], $rawCodes[0]['code']);
}

public function test_decrypts_recovery_codes_if_enabled()
{
$tfa = new TwoFactorAuthentication();

$tfa->encrypted = true;

$recoveryCodes = TwoFactorAuthentication::generateRecoveryCodes(1, 7);

$attributes = $tfa->getAttributes();
$attributes['recovery_codes'] = TwoFactorAuthentication::encryptRecoveryCodes($recoveryCodes);

$tfa->setRawAttributes($attributes);

$this->assertEquals($recoveryCodes->first()['code'], $tfa->recovery_codes->first()['code']);
}

public function test_does_not_decrypt_recovery_codes_if_disabled()
{
$tfa = new TwoFactorAuthentication();

$tfa->encrypted = false;

$recoveryCodes = TwoFactorAuthentication::generateRecoveryCodes(1, 7);

$attributes = $tfa->getAttributes();
$attributes['recovery_codes'] = $recoveryCodes;

$tfa->setRawAttributes($attributes);

$this->assertEquals($recoveryCodes->first()['code'], $tfa->recovery_codes->first()['code']);
}

public function test_lowercases_algorithm()
{
$tfa = TwoFactorAuthentication::factory()
Expand Down

0 comments on commit a16eca3

Please sign in to comment.