Skip to content
This repository has been archived by the owner on Dec 27, 2023. It is now read-only.

Commit

Permalink
Merge branch 'pu/pm/MFAYubicoImpl' into '2021.11'
Browse files Browse the repository at this point in the history
yubico MFA impl

See merge request tine20/tine20!176
  • Loading branch information
paulmhh committed Mar 23, 2021
2 parents af051fe + 4900a47 commit 873d90e
Show file tree
Hide file tree
Showing 12 changed files with 450 additions and 23 deletions.
2 changes: 2 additions & 0 deletions tests/tine20/Tinebase/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@ public function testGetModelsOfAllApplications()
Tinebase_Model_MFA_PinUserConfig::class,
Tinebase_Model_MFA_Config::class,
Tinebase_Model_MFA_UserConfig::class,
Tinebase_Model_MFA_YubicoOTPConfig::class,
Tinebase_Model_MFA_YubicoOTPUserConfig::class,
Tinebase_Model_AuthToken::class,
Tinebase_Model_AuthTokenChannelConfig::class,
Tinebase_Model_BLConfig::class,
Expand Down
63 changes: 63 additions & 0 deletions tests/tine20/Tinebase/Auth/MFATest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,69 @@

class Tinebase_Auth_MFATest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

Tinebase_Auth_MFA::destroyInstances();
}

protected function tearDown(): void
{
parent::tearDown();

Tinebase_Auth_MFA::destroyInstances();
}

public function testYubicoOTP()
{
$this->_originalTestUser->mfa_configs = new Tinebase_Record_RecordSet(
Tinebase_Model_MFA_UserConfig::class, [[
Tinebase_Model_MFA_UserConfig::FLD_ID => 'yubicoOTPunittest',
Tinebase_Model_MFA_UserConfig::FLD_MFA_CONFIG_ID => 'unittest',
Tinebase_Model_MFA_UserConfig::FLD_CONFIG_CLASS =>
Tinebase_Model_MFA_YubicoOTPUserConfig::class,
Tinebase_Model_MFA_UserConfig::FLD_CONFIG =>
new Tinebase_Model_MFA_YubicoOTPUserConfig([
Tinebase_Model_MFA_YubicoOTPUserConfig::FLD_PUBLIC_ID => 'vvccccdhdtnh',
Tinebase_Model_MFA_YubicoOTPUserConfig::FLD_PRIVAT_ID => '1449e1c9cd4c',
Tinebase_Model_MFA_YubicoOTPUserConfig::FLD_AES_KEY => '9a9798f480da0193ab7be4e8abc952c2',
]),
]]);

$this->_createAreaLockConfig([], [
Tinebase_Model_MFA_Config::FLD_ID => 'unittest',
Tinebase_Model_MFA_Config::FLD_USER_CONFIG_CLASS =>
Tinebase_Model_MFA_YubicoOTPUserConfig::class,
Tinebase_Model_MFA_Config::FLD_PROVIDER_CONFIG_CLASS =>
Tinebase_Model_MFA_YubicoOTPConfig::class,
Tinebase_Model_MFA_Config::FLD_PROVIDER_CLASS =>
Tinebase_Auth_MFA_YubicoOTPAdapter::class,
Tinebase_Model_MFA_Config::FLD_PROVIDER_CONFIG => []
]);

$this->_originalTestUser = Tinebase_User::getInstance()->updateUser($this->_originalTestUser);
$mfa = Tinebase_Auth_MFA::getInstance('unittest');

$this->assertFalse($mfa->validate('shaaaaaaaaaalala', $this->_originalTestUser->mfa_configs->getFirstRecord()),
'validate didn\'t fail as expected');
$this->assertTrue($mfa->validate('vvccccdhdtnhleteeguflgbchbgfcbvbclnkknethrfv', $this->_originalTestUser
->mfa_configs->getFirstRecord()), 'validate didn\'t succeed');
$this->_originalTestUser = Tinebase_User::getInstance()->getUserById($this->_originalTestUser->getId(),
Tinebase_Model_FullUser::class);
$this->assertFalse($mfa->validate('vvccccdhdtnhleteeguflgbchbgfcbvbclnkknethrfv', $this->_originalTestUser
->mfa_configs->getFirstRecord()), 'validate didn\'t fail as expected on second call');

$this->assertTrue($mfa->validate('vvccccdhdtnhtrbtrhtbvfldecgjevlutenjkgugglfh', $this->_originalTestUser
->mfa_configs->getFirstRecord()), 'validate didn\'t succeed');
$this->_originalTestUser = Tinebase_User::getInstance()->getUserById($this->_originalTestUser->getId(),
Tinebase_Model_FullUser::class);
$this->assertFalse($mfa->validate('vvccccdhdtnhleteeguflgbchbgfcbvbclnkknethrfv', $this->_originalTestUser
->mfa_configs->getFirstRecord()), 'validate didn\'t fail as expected on second call');
$this->assertFalse($mfa->validate('vvccccdhdtnhtrbtrhtbvfldecgjevlutenjkgugglfh', $this->_originalTestUser
->mfa_configs->getFirstRecord()), 'validate didn\'t succeed');
}

public function testGenericSmsAdapter()
{
$this->_originalTestUser->mfa_configs = new Tinebase_Record_RecordSet(
Expand Down
89 changes: 89 additions & 0 deletions tine20/Tinebase/Auth/MFA/YubicoOTPAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php declare(strict_types=1);
/**
* Tine 2.0
*
* @package Tinebase
* @subpackage Auth
* @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
* @copyright Copyright (c) 2021 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Paul Mehrer <p.mehrer@metaways.de>
*/

use Tinebase_Auth_MFA_YubicoUtil as Yubico;

/**
* Yubico OTP SecondFactor Auth Adapter
*
* @package Tinebase
* @subpackage Auth
*/
class Tinebase_Auth_MFA_YubicoOTPAdapter implements Tinebase_Auth_MFA_AdapterInterface
{
protected $_mfaId;

public function __construct(Tinebase_Record_Interface $_config, string $id)
{
$this->_mfaId = $id;
}

public function sendOut(Tinebase_Model_MFA_UserConfig $_userCfg): bool
{
return true;
}

public function validate($_data, Tinebase_Model_MFA_UserConfig $_userCfg): bool
{
if (!$_userCfg->{Tinebase_Model_MFA_UserConfig::FLD_CONFIG} instanceof Tinebase_Model_MFA_YubicoOTPUserConfig) {
return false;
}
/** @var Tinebase_Model_MFA_YubicoOTPUserConfig $yubicoOTPCfg */
$yubicoOTPCfg = $_userCfg->{Tinebase_Model_MFA_UserConfig::FLD_CONFIG};

if (!preg_match("/^([cbdefghijklnrtuv]{0,16})([cbdefghijklnrtuv]{32})$/", $_data, $matches)) {
return false;
}
$id = $matches[1];
$modhex_ciphertext = $matches[2];

if ($id !== $yubicoOTPCfg->{Tinebase_Model_MFA_YubicoOTPUserConfig::FLD_PUBLIC_ID}) {
return false;
}

/** @var Tinebase_Model_CredentialCache $cc */
$cc = Tinebase_Auth_CredentialCache::getInstance()->get(
$yubicoOTPCfg->{Tinebase_Model_MFA_YubicoOTPUserConfig::FLD_CC_ID});
$cc->key = Tinebase_Config::getInstance()->{Tinebase_Config::CREDENTIAL_CACHE_SHARED_KEY};
Tinebase_Auth_CredentialCache::getInstance()->getCachedCredentials($cc);

$ciphertext = Yubico::modhex2hex($modhex_ciphertext);
$plaintext = Yubico::aes128ecb_decrypt($cc->password, $ciphertext);

if (!Yubico::crc_is_good($plaintext)) {
return false;
}

if (substr($plaintext, 0, 12) !== $cc->username) {
return false;
}
$counter = intval(substr($plaintext, 14, 2) . substr($plaintext, 12, 2), 16);
$session = intval(substr($plaintext, 22, 2), 16);

if ($counter > intval($yubicoOTPCfg->{Tinebase_Model_MFA_YubicoOTPUserConfig::FLD_COUNTER}) || (
$counter === intval($yubicoOTPCfg->{Tinebase_Model_MFA_YubicoOTPUserConfig::FLD_COUNTER}) &&
$session > intval($yubicoOTPCfg->{Tinebase_Model_MFA_YubicoOTPUserConfig::FLD_SESSIONC}))) {
$user = Tinebase_User::getInstance()->getUserById(
$yubicoOTPCfg->{Tinebase_Model_MFA_YubicoOTPUserConfig::FLD_ACCOUNT_ID}, Tinebase_Model_FullUser::class);
if (!($cfg = $user->mfa_configs->getById($_userCfg->getId()))) {
return false;
}
$cfg->{Tinebase_Model_MFA_UserConfig::FLD_CONFIG}->{Tinebase_Model_MFA_YubicoOTPUserConfig::FLD_COUNTER} =
$counter;
$cfg->{Tinebase_Model_MFA_UserConfig::FLD_CONFIG}->{Tinebase_Model_MFA_YubicoOTPUserConfig::FLD_SESSIONC} =
$session;
Tinebase_User::getInstance()->updateUser($user);
return true;
}

return false;
}
}
96 changes: 96 additions & 0 deletions tine20/Tinebase/Auth/MFA/YubicoUtil.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php declare(strict_types=1);
/**
* Tine 2.0
*
* @package Tinebase
* @subpackage Auth
* @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
* @copyright Copyright (c) 2021 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Paul Mehrer <p.mehrer@metaways.de>
*/

# this code is based on https://github.com/Yubico/yubikey-ksm/blob/master/ykksm-utils.php
# Written by Simon Josefsson <simon@josefsson.org>.
# Copyright (c) 2009-2013 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


/**
* Yubico Util class
*
* @package Tinebase
* @subpackage Auth
*/
final class Tinebase_Auth_MFA_YubicoUtil
{
static public function yubi_hex2bin(string $h): string
{
$r = '';
for ($a = 0; $a < strlen($h); $a += 2) {
$r .= chr(hexdec($h[$a] . $h[($a + 1)]));
}
return $r;
}

static public function modhex2hex(string $m): string
{
return strtr($m, "cbdefghijklnrtuv", "0123456789abcdef");
}

static public function aes128ecb_decrypt(string $key, string $in): ?string
{
if (false === ($result = openssl_decrypt(self::yubi_hex2bin($in), 'AES-128-CBC', self::yubi_hex2bin($key),
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, self::yubi_hex2bin('00000000000000000000000000000000')))) {
return null;
}
return bin2hex($result);
}

static public function calculate_crc(string $token): int
{
$crc = 0xffff;

for ($i = 0; $i < 16; $i++) {
$b = hexdec($token[$i * 2] . $token[($i * 2) + 1]);
$crc = $crc ^ ($b & 0xff);
for ($j = 0; $j < 8; $j++) {
$n = $crc & 1;
$crc = $crc >> 1;
if ($n != 0) {
$crc = $crc ^ 0x8408;
}
}
}
return $crc;
}

static public function crc_is_good(string $token): bool
{
$crc = self::calculate_crc($token);
return $crc === 0xf0b8;
}
}
4 changes: 2 additions & 2 deletions tine20/Tinebase/Frontend/Json.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @package Tinebase
* @subpackage Server
* @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
* @copyright Copyright (c) 2007-2020 Metaways Infosystems GmbH (http://www.metaways.de)
* @copyright Copyright (c) 2007-2021 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*
*/
Expand Down Expand Up @@ -39,10 +39,10 @@ class Tinebase_Frontend_Json extends Tinebase_Frontend_Json_Abstract
'ImportExportDefinition',
'LogEntry',
'Tree_Node',
// Tinebase_Model_MFA_Config::MODEL_NAME_PART,
Tinebase_Model_MFA_UserConfig::MODEL_NAME_PART,
Tinebase_Model_MFA_PinUserConfig::MODEL_NAME_PART,
Tinebase_Model_MFA_SmsUserConfig::MODEL_NAME_PART,
Tinebase_Model_MFA_YubicoOTPUserConfig::MODEL_NAME_PART,
];

/**
Expand Down
2 changes: 1 addition & 1 deletion tine20/Tinebase/Model/MFA/PinUserConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

/**
* AuthGenericSmsMFAUserConfig Model
* Pin MFA UserConfig Model
*
* @package Tinebase
* @subpackage Auth
Expand Down
13 changes: 7 additions & 6 deletions tine20/Tinebase/Model/MFA/UserConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,15 @@ class Tinebase_Model_MFA_UserConfig extends Tinebase_Record_NewAbstract
self::CONFIG => [
// not used in client, @see \Admin_Frontend_Json::getPossibleMFAs
// needs to implement Tinebase_Auth_MFA_UserConfigInterface
self::AVAILABLE_MODELS => [
Tinebase_Model_MFA_SmsUserConfig::class,
Tinebase_Model_MFA_PinUserConfig::class,
self::AVAILABLE_MODELS => [
Tinebase_Model_MFA_SmsUserConfig::class,
Tinebase_Model_MFA_PinUserConfig::class,
Tinebase_Model_MFA_YubicoOTPUserConfig::class,
],
],
self::VALIDATORS => [
Zend_Filter_Input::ALLOW_EMPTY => false,
Zend_Filter_Input::PRESENCE => Zend_Filter_Input::PRESENCE_REQUIRED
self::VALIDATORS => [
Zend_Filter_Input::ALLOW_EMPTY => false,
Zend_Filter_Input::PRESENCE => Zend_Filter_Input::PRESENCE_REQUIRED
],
],
self::FLD_CONFIG => [
Expand Down
41 changes: 41 additions & 0 deletions tine20/Tinebase/Model/MFA/YubicoOTPConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php declare(strict_types=1);
/**
* Tine 2.0
*
* @package Tinebase
* @subpackage Auth
* @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
* @copyright Copyright (c) 2021 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Paul Mehrer <p.mehrer@metaways.de>
*/

/**
* Yubico OTP MFA Config Model
*
* @package Tinebase
* @subpackage Auth
*/
class Tinebase_Model_MFA_YubicoOTPConfig extends Tinebase_Auth_MFA_AbstractUserConfig
{
public const MODEL_NAME_PART = 'MFA_YubicoOTPConfig';

/**
* Holds the model configuration (must be assigned in the concrete class)
*
* @var array
*/
protected static $_modelConfiguration = [
self::APP_NAME => Tinebase_Config::APP_NAME,
self::MODEL_NAME => self::MODEL_NAME_PART,

self::FIELDS => [
]
];

/**
* holds the configuration object (must be declared in the concrete class)
*
* @var Tinebase_ModelConfiguration
*/
protected static $_configurationObject = NULL;
}

0 comments on commit 873d90e

Please sign in to comment.