mirror of
https://github.com/Bubka/2FAuth.git
synced 2024-11-25 09:44:04 +01:00
199 lines
6.6 KiB
PHP
199 lines
6.6 KiB
PHP
<?php
|
|
|
|
namespace App\Factories;
|
|
|
|
use App\Exceptions\EncryptedMigrationException;
|
|
use App\Exceptions\UnsupportedMigrationException;
|
|
use App\Services\Migrators\AegisMigrator;
|
|
use App\Services\Migrators\GoogleAuthMigrator;
|
|
use App\Services\Migrators\Migrator;
|
|
use App\Services\Migrators\PlainTextMigrator;
|
|
use App\Services\Migrators\TwoFASMigrator;
|
|
use App\Services\Migrators\TwoFAuthMigrator;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\App;
|
|
use Illuminate\Support\Facades\Validator;
|
|
|
|
class MigratorFactory implements MigratorFactoryInterface
|
|
{
|
|
/**
|
|
* Infer the type of migrator needed from a payload and create the migrator
|
|
*
|
|
* @param string $migrationPayload The migration payload used to infer the migrator type
|
|
*/
|
|
public function create(string $migrationPayload) : Migrator
|
|
{
|
|
if ($this->isTwoFAuthJSON($migrationPayload)) {
|
|
return App::make(TwoFAuthMigrator::class);
|
|
} elseif ($this->isAegisJSON($migrationPayload)) {
|
|
return App::make(AegisMigrator::class);
|
|
} elseif ($this->is2FASv2($migrationPayload)) {
|
|
return App::make(TwoFASMigrator::class);
|
|
} elseif ($this->isGoogleAuth($migrationPayload)) {
|
|
return App::make(GoogleAuthMigrator::class);
|
|
} elseif ($this->isPlainText($migrationPayload)) {
|
|
return App::make(PlainTextMigrator::class);
|
|
} else {
|
|
throw new UnsupportedMigrationException;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine if a payload comes from Google Authenticator
|
|
*
|
|
* @param string $migrationPayload The payload to analyse
|
|
*/
|
|
private function isGoogleAuth(string $migrationPayload) : bool
|
|
{
|
|
// - Google Auth migration URI : a string starting with otpauth-migration://offline?data= on a single line
|
|
|
|
$lines = preg_split('~\R~', $migrationPayload, -1, PREG_SPLIT_NO_EMPTY);
|
|
|
|
if (! $lines || count($lines) != 1) {
|
|
return false;
|
|
}
|
|
|
|
return preg_match('/^otpauth-migration:\/\/offline\?data=.+$/', $lines[0]) == 1;
|
|
}
|
|
|
|
/**
|
|
* Determine if a payload is a plain text content
|
|
*
|
|
* @param string $migrationPayload The payload to analyse
|
|
*/
|
|
private function isPlainText(string $migrationPayload) : bool
|
|
{
|
|
// - Plain text : one or more otpauth URIs (otpauth://(hotp|totp|steam)/...), one per line
|
|
|
|
return Validator::make(
|
|
preg_split('~\R~', $migrationPayload, -1, PREG_SPLIT_NO_EMPTY),
|
|
[
|
|
// The regex rule must be embraced with brackets when it cointains a pipe
|
|
'*' => ['regex:/^otpauth:\/\/(?:steam|totp|hotp)\//i'],
|
|
]
|
|
)->passes();
|
|
}
|
|
|
|
/**
|
|
* Determine if a payload comes from 2FAuth in JSON format
|
|
*
|
|
* @param string $migrationPayload The payload to analyse
|
|
*/
|
|
private function isTwoFAuthJSON(string $migrationPayload) : bool
|
|
{
|
|
$json = json_decode($migrationPayload, true);
|
|
|
|
if (Arr::has($json, 'schema') && (strpos(Arr::get($json, 'app'), '2fauth_') === 0)) {
|
|
return count(Validator::validate(
|
|
$json,
|
|
[
|
|
'data.*.otp_type' => 'present',
|
|
'data.*.service' => 'present',
|
|
'data.*.account' => 'present',
|
|
'data.*.secret' => 'present',
|
|
'data.*.digits' => 'present',
|
|
'data.*.algorithm' => 'present',
|
|
'data.*.period' => 'present',
|
|
'data.*.counter' => 'present',
|
|
]
|
|
)) > 0;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determine if a payload comes from Aegis Authenticator in JSON format
|
|
*
|
|
* @param string $migrationPayload The payload to analyse
|
|
* @return bool
|
|
*/
|
|
private function isAegisJSON(string $migrationPayload) : mixed
|
|
{
|
|
// - Aegis JSON : is a JSON object with the key db.entries full of objects like
|
|
// {
|
|
// "type": "totp",
|
|
// "uuid": "5be1c189-240d-5fe1-930b-a78xb669zd86",
|
|
// "name": "John DOE",
|
|
// "issuer": "Facebook",
|
|
// "note": "",
|
|
// "icon": null,
|
|
// "info": {
|
|
// "secret": "A4GRFTVVRBGY7UIW",
|
|
// "algo": "SHA1",
|
|
// "digits": 6,
|
|
// "period": 30
|
|
// }
|
|
// }
|
|
|
|
$json = json_decode($migrationPayload, true);
|
|
|
|
if (Arr::has($json, 'db')) {
|
|
if (is_string($json['db']) && is_array(Arr::get($json, 'header.slots'))) {
|
|
throw new EncryptedMigrationException;
|
|
} else {
|
|
return count(Validator::validate(
|
|
$json,
|
|
[
|
|
'db.entries.*.type' => 'present',
|
|
'db.entries.*.name' => 'present',
|
|
'db.entries.*.issuer' => 'present',
|
|
'db.entries.*.info' => 'present',
|
|
]
|
|
)) > 0;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determine if a payload comes from 2FAS Authenticator
|
|
*
|
|
* @param string $migrationPayload The payload to analyse
|
|
* @return bool
|
|
*/
|
|
private function is2FASv2(string $migrationPayload) : mixed
|
|
{
|
|
// - 2FAS JSON : is a JSON object with a 'schemaVersion' key and a key 'services' full of objects like
|
|
// {
|
|
// "secret": "A4GRFTVVRBGY7UIW",
|
|
// ...
|
|
// "otp":
|
|
// {
|
|
// "account": "John DOE",
|
|
// "digits": 6,
|
|
// "counter": 0,
|
|
// "period": 30,
|
|
// "algorithm": "SHA1",
|
|
// "tokenType": "TOTP"
|
|
// },
|
|
// "type": "ManuallyAdded",
|
|
// "name": "Facebook",
|
|
// "icon":
|
|
// {
|
|
// ...
|
|
// }
|
|
// }
|
|
|
|
$json = json_decode($migrationPayload, true);
|
|
|
|
if (Arr::has($json, 'schemaVersion') && (Arr::has($json, 'services') || Arr::has($json, 'servicesEncrypted'))) {
|
|
if (Arr::has($json, 'servicesEncrypted')) {
|
|
throw new EncryptedMigrationException;
|
|
} else {
|
|
return count(Validator::validate(
|
|
$json,
|
|
[
|
|
'services.*.secret' => 'present',
|
|
'services.*.name' => 'present',
|
|
'services.*.otp' => 'present',
|
|
]
|
|
)) > 0;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|