Added Logging interface, organized configuration injections

master
Brady McDonough 2 years ago
parent 8b2d419cbb
commit f94331397d

@ -4,7 +4,8 @@
"require": { "require": {
"psr/clock": "1.0.0", "psr/clock": "1.0.0",
"chillerlan/php-qrcode": "5.0.1", "chillerlan/php-qrcode": "5.0.1",
"paragonie/constant_time_encoding": "2.6.3" "paragonie/constant_time_encoding": "2.6.3",
"psr/log": "^3.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11" "phpunit/phpunit": "^11"

@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
function tidy { function tidy {
rm -vf {./,tests/,src/{./,AntiCSRF/,Clock/,Hash/,Request/,Required/,RFC/,Session/,URI/,Workflow/}}*~ rm -vf {./,tests/,src/{./,AntiCSRF/,Clock/,Hash/,Logger/,Request/,Required/,RFC/,Session/,URI/,Workflow/}}*~
rm -vf {./,tests/,src/{./,AntiCSRF/,Clock/,Hash/,Request/,Required/,RFC/,Session/,URI/,Workflow/}}#*# rm -vf {./,tests/,src/{./,AntiCSRF/,Clock/,Hash/,Logger/,Request/,Required/,RFC/,Session/,URI/,Workflow/}}#*#
} }
function clean { function clean {

@ -2,20 +2,13 @@
namespace BradyMcD\TAATP; namespace BradyMcD\TAATP;
use BradyMcD\TAATP\AntiCSRFInterface;
use PSR\Clock\ClockInterface;
use BradyMcD\TAATP\HashInterface;
use BradyMcD\TAATP\Required\PersistenceInterface; use BradyMcD\TAATP\Required\PersistenceInterface;
use BradyMcD\TAATP\Required\RequestInterface; use BradyMcD\TAATP\Required\RequestInterface;
use BradyMcD\TAATP\SessionInterface;
use BradyMcD\TAATP\AntiCSRF\Base as AntiCSRF;
use BradyMcD\TAATP\Clock\Request as RequestClock;
use BradyMcD\TAATP\Clock\Base as Clock;
use BradyMcD\TAATP\Hash\HMAC_SHA1;
use BradyMcD\TAATP\Session\Base as Session;
use BradyMcD\TAATP\Workflow\UserManagement;
use BradyMcD\TAATP\Workflow\Authenticate; use BradyMcD\TAATP\Workflow\Authenticate;
use BradyMcD\TAATP\Workflow\UserManagement;
use BradyMcD\TAATP\HashConfig;
use BradyMcD\TAATP\ModuleConfig;
/** /**
* The primary entrypoint of the module. * The primary entrypoint of the module.
@ -26,21 +19,10 @@ class Factory
public function __construct( public function __construct(
private PersistenceInterface $db, private PersistenceInterface $db,
private RequestInterface $request, private RequestInterface $request,
private ?SessionInterface $session, private ModuleConfig $moduleConfig = new moduleConfig,
private ?ClockInterface $csrfClock, private HashConfig $hashConfig = new HashConfig,
private ?AntiCSRFInterface $csrf,
private ?ClockInterface $totpClock,
private ?HashInterface $hash,
) )
{ {
!is_null($this->session) ? : $this->session = new Session();
!is_null($this->csrfClock) ? : $this->csrfClock = new RequestClock();
!is_null($this->csrf) ? : $this->csrf = new AntiCSRF(
$this->session,
$this->csrfClock
);
!is_null($this->totpClock) ? : $this->totpClock = new Clock();
!is_null($this->hash) ? : $this->hash = new HMAC_SHA1();
} }
public function userManagement(mixed $userIndex): UserManagement public function userManagement(mixed $userIndex): UserManagement
@ -48,10 +30,9 @@ class Factory
return new UserManagement( return new UserManagement(
$this->db, $this->db,
$this->request, $this->request,
$this->csrf,
$this->session,
$userIndex, $userIndex,
$this->hash $this->moduleConfig,
$this->hashConfig
); );
} }
@ -63,10 +44,9 @@ class Factory
return new Authenticate( return new Authenticate(
$this->db, $this->db,
$this->request, $this->request,
$this->csrf,
$this->session,
$userIndex, $userIndex,
$this->hash $this->moduleConfig,
$this->hashConfig
); );
} }
} }

@ -5,14 +5,22 @@ use BradyMcD\TAATP\HashInterface;
use ParagonIE\ConstantTime\Base32; use ParagonIE\ConstantTime\Base32;
/** @SuppressWarnings(PHPMD.CamelCaseClassName)*/ /** @SuppressWarnings(PHPMD.CamelCaseClassName)*/
class HMAC_SHA1 implements HashInterface class BuiltinHash implements HashInterface
{ {
const DEFAULT_SECRET_SIZE = 32; const DEFAULT_SECRET_SIZE = 32;
public function __construct(private readonly string $hashName)
{
(\array_search($this->hashName, \hash_hmac_algos())) ? : throw \ValueError(
$this->hashName . " is not the name of a built-in hash function");
}
/** @SuppressWarnings(PHPMD.StaticAccess) */ /** @SuppressWarnings(PHPMD.StaticAccess) */
public function hash(string $key, string $val): string public function hash(string $key, string $val): string
{ {
$key = Base32::decode($key); $key = Base32::decode($key);
return \hash_hmac("sha1", \hex2bin($val), $key, true); return \hash_hmac($this->hashName, \hex2bin($val), $key, true);
} }
public function hashNumeric(string $key, int $val): string public function hashNumeric(string $key, int $val): string
@ -20,9 +28,9 @@ class HMAC_SHA1 implements HashInterface
return $this->hash($key, \dechex($val)); return $this->hash($key, \dechex($val));
} }
public static function hashType(): string public function hashType(): string
{ {
return "SHA1"; return \strtoupper($this->hashName);
} }
/** @SuppressWarnings(PHPMD.StaticAccess) */ /** @SuppressWarnings(PHPMD.StaticAccess) */

@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace BradyMcD\TAATP;
use Psr\Clock\ClockInterface;
use BradyMcD\TAATP\HashInterface;
use BradyMcD\TAATP\Clock\Base as Clock;
use BradyMcD\TAATP\Hash\BuiltinHash;
/**
* Configures the behaviour of the totp algorithm for newly enrolling users
*/
class HashConfig
{
public function __construct(
public HashInterface|string $algorithm="SHA1",
public ClockInterface $clock=new Clock,
public int $period=30,
public int $digits=6
)
{
if (\is_string($this->hash))
{
$this->hash = new BuiltinHash($this->hash);
}
}
/**
* The Google Authenticator app does not handle customization to the hash function
* In testing as of February 2024 it would not accept otpauth URIs with any field beyond
the secret and issuer, failing with a generic error message
* Currently this is the same as the default constructor but is future proof.
*/
public static function googleAuthenticator(): TotpConfig
{
return new Self(
"SHA1",
new Clock(),
30,
6,
);
}
}
?>

@ -6,7 +6,7 @@ interface HashInterface
{ {
public function hash(string $key, string $val): string; public function hash(string $key, string $val): string;
public function hashNumeric(string $key, int $val): string; public function hashNumeric(string $key, int $val): string;
public static function hashType(): string; public function hashType(): string;
public static function keygen(): string; public static function keygen(): string;
} }

@ -0,0 +1,56 @@
<?php declare(strict_types=1);
use Psr\Log\LoggerInterface;
/** @SuppressWarnings(PHPMD.DevelopmentCodeFragment)*/
class Base implements LoggerInterface
{
public function emergency(string $message, array $context = array())
{
\fwrite(STDERR, $message . \print_r($context, true) . PHP_EOL);
}
public function alert(string $message, array $context = array())
{
\fwrite(STDERR, $message . \print_r($context, true) . PHP_EOL);
}
public function critical(string $message, array $context = array())
{
\fwrite(STDERR, $message . \print_r($context, true) . PHP_EOL);
}
public function error(string $message, array $context = array())
{
\fwrite(STDERR, $message . \print_r($context, true) . PHP_EOL);
}
public function warning(string $message, array $context = array())
{
\fwrite(STDERR, $message . \print_r($context, true) . PHP_EOL);
}
public function notice(string $message, array $context = array())
{
\fwrite(STDOUT, $message . \print_r($context, true) . PHP_EOL);
}
public function info(string $message, array $context = array())
{
\fwrite(STDOUT, $message . \print_r($context, true) . PHP_EOL);
}
public function debug(string $message, array $context = array())
{
\fwrite(STDOUT, $message . \print_r($context, true) . PHP_EOL);
}
public function log(string $message, array $context = array())
{
\fwrite(STDOUT, $message . \print_r($context, true) . PHP_EOL);
}
}
?>

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace BradyMcD\TAATP;
use BradyMcD\TAATP\AntiCSRFInterface;
use Psr\Clock\ClockInterface;
use BradyMcD\TAATP\HashInterface;
use Psr\Log\LoggerInterface;
use BradyMcD\TAATP\Required\PersistenceInterface;
use BradyMcD\TAATP\Required\RequestInterface;
use BradyMcD\TAATP\SessionInterface;
use BradyMcD\TAATP\AntiCSRF\Base as AntiCSRF;
use BradyMcD\TAATP\Clock\Request as RequestClock;
use BradyMcD\TAATP\Clock\Base as Clock;
use BradyMcD\TAATP\Hash\HMAC_SHA1;
use BradyMcD\TAATP\Logger\Base as Logger;
use BradyMcD\TAATP\Session\Base as Session;
/**
* Configures the behaviour of the module overall
*/
class ModuleConfig
{
public function __construct(
public LoggerInterface $logger=new Logger,
public SessionInterface $session=new Session,
public ?AntiCSRFInterface $csrf=null,
)
{
!\is_null($this->csrf) ? : $this->csrf = new AntiCSRF(
$this->session,
new RequestClock()
);
}
}
?>

@ -2,11 +2,10 @@
namespace BradyMcD\TAATP\Workflow; namespace BradyMcD\TAATP\Workflow;
use BradyMcD\TAATP\AntiCSRFInterface;
use BradyMcD\TAATP\HashInterface;
use BradyMcD\TAATP\Required\PersistenceInterface; use BradyMcD\TAATP\Required\PersistenceInterface;
use BradyMcD\TAATP\Required\RequestInterface; use BradyMcD\TAATP\Required\RequestInterface;
use BradyMcD\TAATP\SessionInterface; use BradyMcD\TAATP\HashConfig;
use BradyMcD\TAATP\ModuleConfig;
use BradyMcD\TAATP\WorkflowInterface; use BradyMcD\TAATP\WorkflowInterface;
use BradyMcD\TAATP\URI\Otpauth; use BradyMcD\TAATP\URI\Otpauth;
@ -17,10 +16,9 @@ class Authenticate implements WorkflowInterface
public function __construct( public function __construct(
private PersistenceInterface $db, private PersistenceInterface $db,
private RequestInterface $request, private RequestInterface $request,
private AntiCSRFInterface $csrf,
private SessionInterface $session,
private mixed $userIndex, private mixed $userIndex,
private HashInterface $hash, private ModuleConfig $taatp,
private HashConfig $totp,
) )
{ {
} }
@ -33,14 +31,14 @@ class Authenticate implements WorkflowInterface
function emitStr(): string function emitStr(): string
{ {
$html = "<div id=\"authenticate\"><form %frm>"; $html = "<div id=\"authenticate\"><form %frm>";
$html .= $this->csrf->emitStr(); $html .= $this->taatp.csrf->emitStr();
$html .= "<p>Please enter the code showing on your authenticator</p>"; $html .= "<p>Please enter the code showing on your authenticator</p>";
$html .= "<intput name=\"totp_challenge\" id=\"totp_challenge\" type\"text\" />"; $html .= "<intput name=\"totp_challenge\" id=\"totp_challenge\" type\"text\" />";
$html .= "<input type=\"submit\" value=\"Submit\" />"; $html .= "<input type=\"submit\" value=\"Submit\" />";
$html .= "</form></div>"; $html .= "</form></div>";
$values = [ $values = [
"%frm" => $this->ri->formProps("authenticate"), "%frm" => $this->request->formProps("authenticate"),
]; ];
return \str_replace(\array_keys($values), $values, $html); return \str_replace(\array_keys($values), $values, $html);
@ -48,21 +46,36 @@ class Authenticate implements WorkflowInterface
function response(): bool function response(): bool
{ {
if (!this->csrf->match()) if (!this->taatp.csrf->match())
{ {
$this->taatp.logger->info("Anti-csrf token failure.");
return false; return false;
} }
$pUri = $this->db->getSecret($this->userIndex); $pUri = $this->db->getSecret($this->userIndex);
try
{
$algorithm = new BuiltinHash($pUri.algorithm);
}
catch (\InvalidArgumentException $e)
{
$this->taatp.logger->alert(
"Unable to authenticate user:" .
$db->userString($userIndex) .
". otp configuration on database refers to unsopported algorithm:" .
$pUri.algorithm);
return false;
}
$totp = _6238( $totp = _6238(
$pUri.secret, $pUri.secret,
$pUri.period, $pUri.period,
$this->db->getLastTime($this->userIndex), $this->db->getLastTime($this->userIndex),
2, 2,
$pUri.digits, $pUri.digits,
$this->clock, $this->totp.clock,
$this->hash $algorithm
); );
$flag = $totp.validate($this->ri->getResp("totp_challenge")); $flag = $totp.validate($this->ri->getResp("totp_challenge"));

@ -2,11 +2,10 @@
namespace BradyMcD\TAATP\Workflow; namespace BradyMcD\TAATP\Workflow;
use BradyMcD\TAATP\AntiCSRFInterface;
use BradyMcD\TAATP\HashInterface;
use BradyMcD\TAATP\Required\PersistenceInterface; use BradyMcD\TAATP\Required\PersistenceInterface;
use BradyMcD\TAATP\Required\RequestInterface; use BradyMcD\TAATP\Required\RequestInterface;
use BradyMcD\TAATP\SessionInterface; use BradyMcD\TAATP\HashConfig;
use BradyMcD\TAATP\ModuleConfig;
use BradyMcD\TAATP\WorkflowInterface; use BradyMcD\TAATP\WorkflowInterface;
use BradyMcD\TAATP\URI\Otpauth; use BradyMcD\TAATP\URI\Otpauth;
@ -20,10 +19,9 @@ class UserManagement implements WorkflowInterface
public function __construct( public function __construct(
private PersistenceInterface $db, private PersistenceInterface $db,
private RequestInterface $request, private RequestInterface $request,
private AntiCSRFInterface $csrf,
private SessionInterface $session,
private mixed $userIndex, private mixed $userIndex,
private HashInterface $hash, private ModuleConfig $moduleConfig,
private HashConfig $hashConfig,
) )
{ {
} }
@ -36,7 +34,7 @@ class UserManagement implements WorkflowInterface
private function viewEnrollForm(): string private function viewEnrollForm(): string
{ {
$html = "<div id=\"enroll\"><form %frm>"; $html = "<div id=\"enroll\"><form %frm>";
$html .= $this->csrf->emitStr(); $html .= $this->moduleConfig.csrf->emitStr();
$html .= "<p>To add an authenticator to your account, scan the QR code</p>"; $html .= "<p>To add an authenticator to your account, scan the QR code</p>";
$html .= "<img src=%qr alt=\"qr-code\" height=256px width=256px/>"; $html .= "<img src=%qr alt=\"qr-code\" height=256px width=256px/>";
$html .= "<label for=\"totp_challenge\">Enter the authentication code:</label>"; $html .= "<label for=\"totp_challenge\">Enter the authentication code:</label>";
@ -46,15 +44,15 @@ class UserManagement implements WorkflowInterface
$otpauthURI = new Otpauth( $otpauthURI = new Otpauth(
$this->db->userString($this->userIndex), $this->db->userString($this->userIndex),
"taatp", "taaatp",
$this->hash->keygen(), $this->hashConfig.hash->keygen(),
$this->hash->hashType(), $this->hashConfig.hash->hashType(),
6, 6,
30 30
); );
$provisioningUri = $otpauthURI->uriString(); $provisioningUri = $otpauthURI->uriString();
$persistentUri = $otpauthURI->uriStringExplicit(); $persistentUri = $otpauthURI->uriStringExplicit();
$this->session->store("secret", $persistentUri); $this->moduleConfig.session->store("secret", $persistentUri);
$values = [ $values = [
"%frm" => $this->request->formProps("enroll"), "%frm" => $this->request->formProps("enroll"),
@ -66,7 +64,7 @@ class UserManagement implements WorkflowInterface
private function viewUnenrollForm(): string private function viewUnenrollForm(): string
{ {
$html = "<div id=\"unenroll\"><form %frm>"; $html = "<div id=\"unenroll\"><form %frm>";
$html .= $this->csrf->emitStr(); $html .= $this->moduleConfig.csrf->emitStr();
$html .= "<label for=\"totp_challenge\">To de-register your authenticator enter the current authentication code:</label>"; $html .= "<label for=\"totp_challenge\">To de-register your authenticator enter the current authentication code:</label>";
$html .= "<intput name=\"totp_challenge\" id=\"totp_challenge\" type\"text\" />"; $html .= "<intput name=\"totp_challenge\" id=\"totp_challenge\" type\"text\" />";
$html .= "<input type=\"submit\" value=\"Submit\" />"; $html .= "<input type=\"submit\" value=\"Submit\" />";
@ -90,25 +88,26 @@ class UserManagement implements WorkflowInterface
function response(): bool function response(): bool
{ {
if (!$this->csrf->match()) if (!$this->moduleConfig.csrf->match())
{ {
return false; return false;
} }
$pUri = $this->db->getSecret($this->userIndex); $pUri = $this->db->getSecret($this->userIndex);
$enrollFlag = \is_null($pUri); $enrollFlag = \is_null($pUri);
$enrollFlag && $pUri = $this->session->get('secret'); $enrollFlag && $pUri = $this->moduleConfig.session->get('secret');
echo "Recovered pURI for a " . ($enrollFlag)?"new user":"returning user"; echo "Recovered pURI for a " . ($enrollFlag)?"new user":"returning user";
$totp = _6238( $totp = _6238(
$pUri->getSecret(), $pUri->getSecret(),
$pUri.period, $pUri.period,
$enrollFlag? 0:$this->db->getLastTime($this->userIndex), $enrollFlag? 0:$this->db->getLastTime($this->userIndex),
2, 2,
$pUri.digits, $pUri.digits,
$this->clock, $this->hashConfig.clock,
$this->hash $this->hashConfig.algorithm
); );
$flag = $totp.validate($this->request->getResp("totp_challenge")); $flag = $totp.validate($this->request->getResp("totp_challenge"));

Loading…
Cancel
Save