diff --git a/providers/class.two-factor-totp.php b/providers/class.two-factor-totp.php index eb4c1dfd..cc1f6399 100644 --- a/providers/class.two-factor-totp.php +++ b/providers/class.two-factor-totp.php @@ -41,6 +41,26 @@ protected function __construct() { return parent::__construct(); } + private static $now; + + /** + * Override time() in the current object for testing. + * + * @return int + */ + private static function time() { + return self::$now ?: time(); + } + + /** + * Set up the internal state of time() invocations for deterministic generation. + * + * @param int $now Timestamp to use when overriding time(). + */ + public static function __set_time( $now ) { + self::$now = $now; + } + /** * Ensures only one instance of this class exists in memory at any one time. */ @@ -196,7 +216,7 @@ public static function is_valid_authcode( $key, $authcode, $hash = 'sha1', $time $ticks = range( - $max_ticks, $max_ticks ); usort( $ticks, array( __CLASS__, 'abssort' ) ); - $time = time() / self::DEFAULT_TIME_STEP_SEC; + $time = self::time() / self::DEFAULT_TIME_STEP_SEC; $digits = strlen( $authcode ); @@ -312,7 +332,7 @@ public static function calc_totp( $key, $step_count = false, $digits = self::DEF } if ( false === $step_count ) { - $step_count = floor( time() / $time_step ); + $step_count = floor( self::time() / $time_step ); } $timestamp = self::pack64( $step_count ); diff --git a/tests/providers/class.two-factor-totp.php b/tests/providers/class.two-factor-totp.php index 6477b499..baf3ccd1 100644 --- a/tests/providers/class.two-factor-totp.php +++ b/tests/providers/class.two-factor-totp.php @@ -5,6 +5,21 @@ class Tests_Two_Factor_Totp extends WP_UnitTestCase { + private static $token = '12345678901234567890'; + private static $step = 30; + + private static $vectors = [ + 59 => ['94287082', '46119246', '90693936'], + 1111111109 => ['07081804', '68084774', '25091201'], + 1111111111 => ['14050471', '67062674', '99943326'], + 1234567890 => ['89005924', '91819424', '93441116'], + 2000000000 => ['69279037', '90698825', '38618901'], + 20000000000 => ['65353130', '77737706', '47863826'] + ]; + + /** + * @var Two_Factor_Totp + */ protected $provider; /** @@ -192,4 +207,121 @@ public function test_is_valid_authcode() { $this->assertTrue( $this->provider->is_valid_authcode( $key, $authcode ) ); } + /** + * @covers Two_Factor_Totp::calc_totp + */ + public function test_sha1_generate() { + if ( PHP_INT_SIZE === 4 ) { + $this->markTestSkipped( 'calc_totp requires 64-bit PHP' ); + } + + $provider = $this->provider; + $hash = 'sha1'; + $token = $provider->base32_encode( self::$token ); + + foreach (self::$vectors as $time => $vector) { + $provider::__set_time( (int) $time ); + $this->assertEquals( $vector[0], $provider::calc_totp( $token, false, 8, $hash, self::$step ) ); + $this->assertEquals( substr( $vector[0], 2 ), $provider::calc_totp( $token, false, 6, $hash, self::$step ) ); + } + } + + /** + * @covers Two_Factor_Totp::is_valid_authcode + * @covers Two_Factor_Totp::calc_totp + */ + public function test_sha1_authenticate() { + if ( PHP_INT_SIZE === 4 ) { + $this->markTestSkipped( 'calc_totp requires 64-bit PHP' ); + } + + $provider = $this->provider; + $hash = 'sha1'; + $token = $provider->base32_encode( self::$token ); + + foreach ( self::$vectors as $time => $vector ) { + $provider::__set_time( (int) $time ); + $this->assertTrue( $provider::is_valid_authcode( $token, $vector[0], $hash ) ); + $this->assertTrue( $provider::is_valid_authcode( $token, substr( $vector[0], 2 ), $hash ) ); + } + } + + /** + * @covers Two_Factor_Totp::calc_totp + */ + public function test_sha256_generate() { + if (PHP_INT_SIZE === 4) { + $this->markTestSkipped( 'calc_totp requires 64-bit PHP' ); + } + + $provider = $this->provider; + $hash = 'sha256'; + $token = $provider->base32_encode( self::$token ); + + foreach ( self::$vectors as $time => $vector ) { + $provider::__set_time( (int) $time ); + $this->assertEquals( $vector[1], $provider::calc_totp( $token, false, 8, $hash, self::$step ) ); + $this->assertEquals( substr( $vector[1], 2 ), $provider::calc_totp( $token, false, 6, $hash, self::$step ) ); + } + } + + /** + * @covers Two_Factor_Totp::is_valid_authcode + * @covers Two_Factor_Totp::calc_totp + */ + public function test_sha256_authenticate() { + if ( PHP_INT_SIZE === 4 ) { + $this->markTestSkipped( 'calc_totp requires 64-bit PHP' ); + } + + $provider = $this->provider; + $hash = 'sha256'; + $token = $provider->base32_encode( self::$token ); + + foreach ( self::$vectors as $time => $vector ) { + $provider::__set_time( (int) $time ); + $this->assertTrue( $provider::is_valid_authcode( $token, $vector[1], $hash ) ); + $this->assertTrue( $provider::is_valid_authcode( $token, substr( $vector[1], 2 ), $hash ) ); + } + } + + /** + * @covers Two_Factor_Totp::calc_totp + */ + public function test_sha512_generate() { + if ( PHP_INT_SIZE === 4 ) { + $this->markTestSkipped('calc_totp requires 64-bit PHP'); + } + + $provider = $this->provider; + $hash = 'sha512'; + $token = $provider->base32_encode( self::$token ); + + foreach ( self::$vectors as $time => $vector ) { + $provider::__set_time( (int) $time ); + $this->assertEquals( $vector[2], $provider::calc_totp( $token, false, 8, $hash, self::$step ) ); + $this->assertEquals( substr($vector[2], 2 ), $provider::calc_totp( $token, false, 6, $hash, self::$step ) ); + } + } + + /** + * @covers Two_Factor_Totp::is_valid_authcode + * @covers Two_Factor_Totp::calc_totp + */ + public function test_sha512_authenticate() { + if ( PHP_INT_SIZE === 4 ) { + $this->markTestSkipped( 'calc_totp requires 64-bit PHP' ); + } + + $provider = $this->provider; + $hash = 'sha512'; + $token = $provider->base32_encode( self::$token ); + + foreach ( self::$vectors as $time => $vector ) { + $provider::__set_time( (int) $time ); + $this->assertTrue( $provider::is_valid_authcode( $token, $vector[2], $hash ) ); + $this->assertTrue( $provider::is_valid_authcode( $token, substr( $vector[2], 2 ), $hash ) ); + } + + } }