diff --git a/README.md b/README.md index edf3dc4d5..971ecae78 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,52 @@ The app is available through the [app store](https://apps.nextcloud.com/apps/two ![](screenshots/enter_challenge.png) ![](screenshots/settings.png) +## Setting options for security hardening + +The secret generated by this application is 32 characters long and includes only the characters A-Z and the numbers 2, 3, 4, 5, 6, and 7 ([RFC 4648 Base32 alphabet](https://datatracker.ietf.org/doc/html/rfc4648#section-6)), meeting the recommended 160-bit depth by [RFC 4226](https://datatracker.ietf.org/doc/html/rfc4226#section-4). +Every 30 seconds, a 6-character numeric one-time password (OTP) is generated using the SHA-1 hash algorithm on both - the server and the TOTP authenticator app on your smartphone /-device. So whoever has the secret can get access to the second factor. That's why TOTP apps are usually very well secured, at least with a master password or passphrase. +Since the OTP only grants access if exactly the same hash algorithm and the same token length are set on the server as the TOTP authenticator app, there is the possibility to change these values. In the vast majority of cases, these options are not needed to be touched at all. They are only intended for cases with extremely high security requirements. + +### Configurable Options + +1. **Secret Length:** + * Default: 32 characters (160 bits) + * Minimum: 26 characters (130 bits) + * Maximum: 128 characters (640 bits) + + Adjust the secret length using: + + ```sh + occ config:app:set --value=64 -- twofactor_totp secret_length + ``` + +2. **Hash Algorithm:** + * Default: [SHA-1](https://datatracker.ietf.org/doc/html/rfc4226#appendix-B.1) (*`sha1`*) + * Optionally use SHA-256 (*`sha256`*) or SHA-512 (*`sha512`*): + + ```sh + occ config:app:set --value=sha512 -- twofactor_totp hash_algorithm + ``` + +3. **Token Length:** + * Default: 6 digits + * Minimum: 6 digits + * Maximum: 12 digits + + Set the token length using: + + ```sh + occ config:app:set --value=9 -- twofactor_totp token_length + ``` + + +### Considerations +* The secret length affects only the initial generation of secrets for users. Once generated, secrets are encrypted and stored in the database and unencrypted stored in the TOTP app/device. Changing the secret length afterwards does not affect previously generated secrets. + +* Similarly, token length and hash algorithm can be changed at any time using the provided commands. However, these changes must be synchronized with all users of TOTP-enabled accounts. + +* Note that not all TOTP apps support multiple token lengths or hash algorithms. The Aegis app, however, supports all configurations mentioned here. + ## Login with external apps Once you enable OTP with Two Factor Totp, your aplications (for example your Android app or your GNOME app) will need to login using device passwords. To manage it, [know more here](https://docs.nextcloud.com/server/stable/user_manual/en/session_management.html#managing-devices) @@ -40,3 +86,4 @@ Once you enable OTP with Two Factor Totp, your aplications (for example your And * `composer i` * `npm ci` * `npm run build` or `npm run dev` [more info](https://docs.nextcloud.com/server/latest/developer_manual/digging_deeper/npm.html) + diff --git a/lib/Service/Totp.php b/lib/Service/Totp.php index eef5ff6d4..cd00f535e 100644 --- a/lib/Service/Totp.php +++ b/lib/Service/Totp.php @@ -26,7 +26,9 @@ use Base32\Base32; use EasyTOTP\Factory; +use EasyTOTP\TOTPInterface; use EasyTOTP\TOTPValidResultInterface; +use OCA\TwoFactorTOTP\AppInfo\Application; use OCA\TwoFactorTOTP\Db\TotpSecret; use OCA\TwoFactorTOTP\Db\TotpSecretMapper; use OCA\TwoFactorTOTP\Event\DisabledByAdmin; @@ -34,11 +36,20 @@ use OCA\TwoFactorTOTP\Exception\NoTotpSecretFoundException; use OCP\AppFramework\Db\DoesNotExistException; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\IUser; use OCP\Security\ICrypto; use OCP\Security\ISecureRandom; class Totp implements ITotp { + private const DEFAULT_SECRET_LENGTH = 32; + private const DEFAULT_HASH_ALGORITHM = TOTPInterface::HASH_SHA1; + private const DEFAULT_TOKEN_LENGTH = 6; + + private const MIN_SECRET_LENGTH = 26; + private const MAX_SECRET_LENGTH = 128; + private const MIN_TOKEN_LENGTH = 6; + private const MAX_TOKEN_LENGTH = 12; /** @var TotpSecretMapper */ private $secretMapper; @@ -52,14 +63,34 @@ class Totp implements ITotp { /** @var ISecureRandom */ private $random; + /** @var IConfig */ + private $config; + public function __construct(TotpSecretMapper $secretMapper, ICrypto $crypto, IEventDispatcher $eventDispatcher, - ISecureRandom $random) { + ISecureRandom $random, + IConfig $config) { $this->secretMapper = $secretMapper; $this->crypto = $crypto; $this->eventDispatcher = $eventDispatcher; $this->random = $random; + $this->config = $config; + } + + private function getSecretLength(): int { + $length = (int)$this->config->getAppValue(Application::APP_ID, 'secret_length', (string) self::DEFAULT_SECRET_LENGTH); + return ($length >= self::MIN_SECRET_LENGTH && $length <= self::MAX_SECRET_LENGTH) ? $length : self::DEFAULT_SECRET_LENGTH; + } + + private function getHashAlgorithm(): string { + $algorithm = strtolower($this->config->getAppValue(Application::APP_ID, 'hash_algorithm', self::DEFAULT_HASH_ALGORITHM)); + return in_array($algorithm, [TOTPInterface::HASH_SHA1, TOTPInterface::HASH_SHA256, TOTPInterface::HASH_SHA512], true) ? $algorithm : self::DEFAULT_HASH_ALGORITHM; + } + + private function getTokenLength(): int { + $length = (int)$this->config->getAppValue(Application::APP_ID, 'token_length', (string) self::DEFAULT_TOKEN_LENGTH); + return ($length >= self::MIN_TOKEN_LENGTH && $length <= self::MAX_TOKEN_LENGTH) ? $length : self::DEFAULT_TOKEN_LENGTH; } public function hasSecret(IUser $user): bool { @@ -72,7 +103,8 @@ public function hasSecret(IUser $user): bool { } private function generateSecret(): string { - return $this->random->generate(32, ISecureRandom::CHAR_UPPER.'234567'); + $secretLength = $this->getSecretLength(); + return $this->random->generate($secretLength, ISecureRandom::CHAR_UPPER.'234567'); } /** @@ -136,7 +168,9 @@ public function validateSecret(IUser $user, string $key): bool { } $secret = $this->crypto->decrypt($dbSecret->getSecret()); - $otp = Factory::getTOTP(Base32::decode($secret), 30, 6); + $hashAlgorithm = $this->getHashAlgorithm(); + $tokenLength = $this->getTokenLength(); + $otp = Factory::getTOTP(Base32::decode($secret), 30, $tokenLength, 0, $hashAlgorithm); $counter = null; $lastCounter = $dbSecret->getLastCounter();