Skip to content
4 changes: 2 additions & 2 deletions apps/comments/composer/composer/installed.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
'reference' => '85618b795280107ce542bff646bc0957bc09a452',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
Expand All @@ -13,7 +13,7 @@
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
'reference' => '85618b795280107ce542bff646bc0957bc09a452',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
Expand Down
40 changes: 20 additions & 20 deletions apps/twofactor_backupcodes/lib/Db/BackupCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,24 @@

namespace OCA\TwoFactorBackupCodes\Db;

use OCP\AppFramework\Db\Entity;

/**
* @method string getUserId()
* @method void setUserId(string $userId)
* @method string getCode()
* @method void setCode(string $code)
* @method int getUsed()
* @method void setUsed(int $code)
*/
class BackupCode extends Entity {

/** @var string */
protected $userId;

/** @var string */
protected $code;

/** @var int */
protected $used;
use OCP\AppFramework\ORM\Attribute\Column;
use OCP\AppFramework\ORM\Attribute\Entity;
use OCP\AppFramework\ORM\Attribute\Id;
use OCP\DB\Types;
use OCP\Snowflake\ISnowflakeGenerator;

#[Entity(name: 'twofactor_backupcodes')]
final class BackupCode {
#[Id(generatorClass: ISnowflakeGenerator::class)]
#[Column(name: 'id', type: Types::STRING, length: 64, nullable: false)]
public ?string $id = null;

#[Column(name: 'user_id', type: Types::STRING, length: 64, nullable: false)]
public string $userId;

#[Column(name: 'code', type: Types::STRING, length: 128, nullable: false)]
public string $code;

#[Column(name: 'used', type: Types::SMALLINT, nullable: false, default: 0)]
public int $used = 0;
}
59 changes: 24 additions & 35 deletions apps/twofactor_backupcodes/lib/Db/BackupCodeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,62 +9,51 @@

namespace OCA\TwoFactorBackupCodes\Db;

use OCP\AppFramework\Db\QBMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\ORM\Repository;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;

/**
* @template-extends QBMapper<BackupCode>
* @template-extends Repository<BackupCode>
*/
class BackupCodeMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'twofactor_backupcodes');
}
class BackupCodeMapper extends Repository {
public const string entityClass = BackupCode::class;

/**
* @param IUser $user
* @return BackupCode[]
* @return \Generator<BackupCode>
*/
public function getBackupCodes(IUser $user): array {
/* @var IQueryBuilder $qb */
$qb = $this->db->getQueryBuilder();

$qb->select('id', 'user_id', 'code', 'used')
->from('twofactor_backupcodes')
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID())));

return self::findEntities($qb);
public function findByUser(IUser $user): \Generator {
return $this->findBy([
'userId' => $user->getUID(),
]);
}

/**
* @param IUser $user
*/
public function deleteCodes(IUser $user): void {
$this->deleteCodesByUserId($user->getUID());
public function deleteByUser(IUser $user): void {
$this->deleteBy([
'userId' => $user->getUID(),
]);
}

/**
* @param string $uid
*/
public function deleteCodesByUserId(string $uid): void {
/* @var IQueryBuilder $qb */
$qb = $this->db->getQueryBuilder();

$qb->delete('twofactor_backupcodes')
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($uid)));
$qb->executeStatement();
public function findOneByUser(IUser $user): ?BackupCode {
try {
return $this->findOneBy([
'userId' => $user->getUID(),
]);
} catch (DoesNotExistException) {
return null;
}
}

/**
* Marks the backup code as used, if not already marked as used in DB.
* @return int number of affected rows
*/
public function markUsedIfUnused(BackupCode $code): int {
$qb = $this->db->getQueryBuilder();
$qb = $this->connection->getQueryBuilder();
$qb->update($this->getTableName())
->set('used', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))
->where($qb->expr()->eq('id', $qb->createNamedParameter($code->getId(), IQueryBuilder::PARAM_INT)))
->where($qb->expr()->eq('id', $qb->createNamedParameter($code->id, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('used', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
return $qb->executeStatement();
}
Expand Down
2 changes: 1 addition & 1 deletion apps/twofactor_backupcodes/lib/Listener/UserDeleted.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ public function handle(Event $event): void {
return;
}

$this->backupCodeMapper->deleteCodes($event->getUser());
$this->backupCodeMapper->deleteByUser($event->getUser());
}
}
31 changes: 16 additions & 15 deletions apps/twofactor_backupcodes/lib/Service/BackupCodeStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ public function createCodes(IUser $user, int $number = 10): array {
$result = [];

// Delete existing ones
$this->mapper->deleteCodes($user);
$this->mapper->deleteByUser($user);

$uid = $user->getUID();
foreach (range(1, min([$number, 20])) as $i) {
$code = $this->random->generate(self::CODE_LENGTH, ISecureRandom::CHAR_HUMAN_READABLE);

$dbCode = new BackupCode();
$dbCode->setUserId($uid);
$dbCode->setCode($this->hasher->hash($code));
$dbCode->setUsed(0);
$dbCode->userId = $uid;
$dbCode->code = $this->hasher->hash($code);
$dbCode->used = 0;
$this->mapper->insert($dbCode);

$result[] = $code;
Expand All @@ -62,23 +62,24 @@ public function createCodes(IUser $user, int $number = 10): array {
* @return bool
*/
public function hasBackupCodes(IUser $user): bool {
$codes = $this->mapper->getBackupCodes($user);
return count($codes) > 0;
return $this->mapper->findOneByUser($user) !== null;
}

/**
* @param IUser $user
* @return array
*/
public function getBackupCodesState(IUser $user): array {
$codes = $this->mapper->getBackupCodes($user);
$total = count($codes);
$codes = $this->mapper->findByUser($user);
$total = 0;
$used = 0;
array_walk($codes, function (BackupCode $code) use (&$used): void {
if ((int)$code->getUsed() === 1) {

foreach ($codes as $code) {
$total++;
if ($code->used === 1) {
$used++;
}
});
}
return [
'enabled' => $total > 0,
'total' => $total,
Expand All @@ -87,17 +88,17 @@ public function getBackupCodesState(IUser $user): array {
}

public function validateCode(IUser $user, string $code): bool {
$dbCodes = $this->mapper->getBackupCodes($user);
$dbCodes = $this->mapper->findByUser($user);

foreach ($dbCodes as $dbCode) {
if ((int)$dbCode->getUsed() === 0 && $this->hasher->verify($code, $dbCode->getCode())) {
return ($this->mapper->markUsedIfUnused($dbCode) === 1);
if ($dbCode->used === 0 && $this->hasher->verify($code, $dbCode->code)) {
return $this->mapper->markUsedIfUnused($dbCode) === 1;
}
}
return false;
}

public function deleteCodes(IUser $user): void {
$this->mapper->deleteCodes($user);
$this->mapper->deleteByUser($user);
}
}
34 changes: 17 additions & 17 deletions apps/twofactor_backupcodes/tests/Db/BackupCodeMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ protected function tearDown(): void {

public function testGetBackupCodes(): void {
$code1 = new BackupCode();
$code1->setUserId($this->testUID);
$code1->setCode('1|$2y$10$Fyo.DkMtkaHapVvRVbQBeeIdi5x/6nmPnxiBzD0GDKa08NMus5xze');
$code1->setUsed(1);
$code1->userId = $this->testUID;
$code1->code = '1|$2y$10$Fyo.DkMtkaHapVvRVbQBeeIdi5x/6nmPnxiBzD0GDKa08NMus5xze';
$code1->used = 1;

$code2 = new BackupCode();
$code2->setUserId($this->testUID);
$code2->setCode('1|$2y$10$nj3sZaCqGN8t6.SsnNADt.eX34UCkdX6FPx.r.rIwE6Jj3vi5wyt2');
$code2->setUsed(0);
$code2->userId = $this->testUID;
$code2->code = '1|$2y$10$nj3sZaCqGN8t6.SsnNADt.eX34UCkdX6FPx.r.rIwE6Jj3vi5wyt2';
$code2->used = 0;

$this->mapper->insert($code1);
$this->mapper->insert($code2);
Expand All @@ -63,7 +63,7 @@ public function testGetBackupCodes(): void {
->method('getUID')
->willReturn($this->testUID);

$dbCodes = $this->mapper->getBackupCodes($user);
$dbCodes = iterator_to_array($this->mapper->findByUser($user));

$this->assertCount(2, $dbCodes);
$this->assertInstanceOf(BackupCode::class, $dbCodes[0]);
Expand All @@ -72,34 +72,34 @@ public function testGetBackupCodes(): void {

public function testDeleteCodes(): void {
$code = new BackupCode();
$code->setUserId($this->testUID);
$code->setCode('1|$2y$10$CagG8pEhZL.xDirtCCP/KuuWtnsAasgq60zY9rU46dBK4w8yW0Z/y');
$code->setUsed(1);
$code->userId = $this->testUID;
$code->code = '1|$2y$10$CagG8pEhZL.xDirtCCP/KuuWtnsAasgq60zY9rU46dBK4w8yW0Z/y';
$code->used = 1;
$user = $this->getMockBuilder(IUser::class)->getMock();
$user->expects($this->any())
->method('getUID')
->willReturn($this->testUID);

$this->mapper->insert($code);

$this->assertCount(1, $this->mapper->getBackupCodes($user));
$this->assertCount(1, iterator_to_array($this->mapper->findByUser($user)));

$this->mapper->deleteCodes($user);
$this->mapper->deleteByUser($user);

$this->assertCount(0, $this->mapper->getBackupCodes($user));
$this->assertCount(0, iterator_to_array($this->mapper->findByUser($user)));
}

public function testInsertArgonEncryptedCodes(): void {
$code = new BackupCode();
$code->setUserId($this->testUID);
$code->setCode('2|$argon2i$v=19$m=1024,t=2,p=2$MjJWUjRFWndtMm5BWGxOag$BusVxLeFyiLLWtaVvX/JRFBiPdZcjRrzpQ/rAhn6vqY');
$code->setUsed(1);
$code->userId = $this->testUID;
$code->code = '2|$argon2i$v=19$m=1024,t=2,p=2$MjJWUjRFWndtMm5BWGxOag$BusVxLeFyiLLWtaVvX/JRFBiPdZcjRrzpQ/rAhn6vqY';
$code->used = 1;
$user = $this->getMockBuilder(IUser::class)->getMock();
$user->expects($this->any())
->method('getUID')
->willReturn($this->testUID);

$this->mapper->insert($code);
$this->assertCount(1, $this->mapper->getBackupCodes($user));
$this->assertCount(1, iterator_to_array($this->mapper->findByUser($user)));
}
}
Loading
Loading