BackupCodeStorage.php 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCA\TwoFactorBackupCodes\Service;
  8. use OCA\TwoFactorBackupCodes\Db\BackupCode;
  9. use OCA\TwoFactorBackupCodes\Db\BackupCodeMapper;
  10. use OCA\TwoFactorBackupCodes\Event\CodesGenerated;
  11. use OCP\EventDispatcher\IEventDispatcher;
  12. use OCP\IUser;
  13. use OCP\Security\IHasher;
  14. use OCP\Security\ISecureRandom;
  15. class BackupCodeStorage {
  16. private static $CODE_LENGTH = 16;
  17. /** @var BackupCodeMapper */
  18. private $mapper;
  19. /** @var IHasher */
  20. private $hasher;
  21. /** @var ISecureRandom */
  22. private $random;
  23. /** @var IEventDispatcher */
  24. private $eventDispatcher;
  25. public function __construct(BackupCodeMapper $mapper,
  26. ISecureRandom $random,
  27. IHasher $hasher,
  28. IEventDispatcher $eventDispatcher) {
  29. $this->mapper = $mapper;
  30. $this->hasher = $hasher;
  31. $this->random = $random;
  32. $this->eventDispatcher = $eventDispatcher;
  33. }
  34. /**
  35. * @param IUser $user
  36. * @param int $number
  37. * @return string[]
  38. */
  39. public function createCodes(IUser $user, int $number = 10): array {
  40. $result = [];
  41. // Delete existing ones
  42. $this->mapper->deleteCodes($user);
  43. $uid = $user->getUID();
  44. foreach (range(1, min([$number, 20])) as $i) {
  45. $code = $this->random->generate(self::$CODE_LENGTH, ISecureRandom::CHAR_HUMAN_READABLE);
  46. $dbCode = new BackupCode();
  47. $dbCode->setUserId($uid);
  48. $dbCode->setCode($this->hasher->hash($code));
  49. $dbCode->setUsed(0);
  50. $this->mapper->insert($dbCode);
  51. $result[] = $code;
  52. }
  53. $this->eventDispatcher->dispatchTyped(new CodesGenerated($user));
  54. return $result;
  55. }
  56. /**
  57. * @param IUser $user
  58. * @return bool
  59. */
  60. public function hasBackupCodes(IUser $user): bool {
  61. $codes = $this->mapper->getBackupCodes($user);
  62. return count($codes) > 0;
  63. }
  64. /**
  65. * @param IUser $user
  66. * @return array
  67. */
  68. public function getBackupCodesState(IUser $user): array {
  69. $codes = $this->mapper->getBackupCodes($user);
  70. $total = count($codes);
  71. $used = 0;
  72. array_walk($codes, function (BackupCode $code) use (&$used) {
  73. if ((int)$code->getUsed() === 1) {
  74. $used++;
  75. }
  76. });
  77. return [
  78. 'enabled' => $total > 0,
  79. 'total' => $total,
  80. 'used' => $used,
  81. ];
  82. }
  83. /**
  84. * @param IUser $user
  85. * @param string $code
  86. * @return bool
  87. */
  88. public function validateCode(IUser $user, string $code): bool {
  89. $dbCodes = $this->mapper->getBackupCodes($user);
  90. foreach ($dbCodes as $dbCode) {
  91. if ((int)$dbCode->getUsed() === 0 && $this->hasher->verify($code, $dbCode->getCode())) {
  92. $dbCode->setUsed(1);
  93. $this->mapper->update($dbCode);
  94. return true;
  95. }
  96. }
  97. return false;
  98. }
  99. public function deleteCodes(IUser $user): void {
  100. $this->mapper->deleteCodes($user);
  101. }
  102. }