From fe09582eb60f16f33ad9add32f3178a89e17fda2 Mon Sep 17 00:00:00 2001 From: ernolf Date: Fri, 12 Jul 2024 09:56:36 +0200 Subject: [PATCH 1/6] add 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: occ config:app:set --value=64 -- twofactor_totp secret_length 2. Hash Algorithm: * Default: SHA-1 (sha1) * Optionally use SHA-256 (sha256) or SHA-512 (sha512): 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: occ config:app:set --value=9 -- twofactor_totp token_length Signed-off-by: ernolf --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++ lib/Service/Totp.php | 41 +++++++++++++++++++++++++++++++----- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index edf3dc4d5..1222141d5 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,55 @@ 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 +89,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..770223b2f 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; @@ -37,8 +39,17 @@ use OCP\IUser; use OCP\Security\ICrypto; use OCP\Security\ISecureRandom; +use OCP\IConfig; 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; @@ -53,13 +64,30 @@ class Totp implements ITotp { private $random; public function __construct(TotpSecretMapper $secretMapper, - ICrypto $crypto, - IEventDispatcher $eventDispatcher, - ISecureRandom $random) { + ICrypto $crypto, + IEventDispatcher $eventDispatcher, + 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', 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 = $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', 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 +100,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 +165,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(); From 4d2e7b731bdf921664787dfde79613686421a30d Mon Sep 17 00:00:00 2001 From: ernolf Date: Fri, 12 Jul 2024 11:56:43 +0200 Subject: [PATCH 2/6] make hash algorithm case insensitive Signed-off-by: ernolf --- lib/Service/Totp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/Totp.php b/lib/Service/Totp.php index 770223b2f..69171e278 100644 --- a/lib/Service/Totp.php +++ b/lib/Service/Totp.php @@ -81,7 +81,7 @@ private function getSecretLength(): int { } private function getHashAlgorithm(): string { - $algorithm = $this->config->getAppValue(Application::APP_ID, 'hash_algorithm', self::DEFAULT_HASH_ALGORITHM); + $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; } From 46b903f4f1361516d90aab2a36783161fce52d2b Mon Sep 17 00:00:00 2001 From: ernolf Date: Fri, 12 Jul 2024 21:37:37 +0200 Subject: [PATCH 3/6] Fix constructor indentation (Lint) Signed-off-by: ernolf --- lib/Service/Totp.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Service/Totp.php b/lib/Service/Totp.php index 69171e278..cc5776c3d 100644 --- a/lib/Service/Totp.php +++ b/lib/Service/Totp.php @@ -36,10 +36,10 @@ 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; -use OCP\IConfig; class Totp implements ITotp { private const DEFAULT_SECRET_LENGTH = 32; @@ -64,10 +64,10 @@ class Totp implements ITotp { private $random; public function __construct(TotpSecretMapper $secretMapper, - ICrypto $crypto, - IEventDispatcher $eventDispatcher, - ISecureRandom $random, - IConfig $config) { + ICrypto $crypto, + IEventDispatcher $eventDispatcher, + ISecureRandom $random, + IConfig $config) { $this->secretMapper = $secretMapper; $this->crypto = $crypto; $this->eventDispatcher = $eventDispatcher; From 591847f29b8cb5006455e60f074eb56301fb8203 Mon Sep 17 00:00:00 2001 From: ernolf Date: Fri, 12 Jul 2024 21:52:36 +0200 Subject: [PATCH 4/6] Add type declarations for class properties in Totp.php Signed-off-by: ernolf --- lib/Service/Totp.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Service/Totp.php b/lib/Service/Totp.php index cc5776c3d..1a9186284 100644 --- a/lib/Service/Totp.php +++ b/lib/Service/Totp.php @@ -63,6 +63,9 @@ class Totp implements ITotp { /** @var ISecureRandom */ private $random; + /** @var IConfig */ + private $config; + public function __construct(TotpSecretMapper $secretMapper, ICrypto $crypto, IEventDispatcher $eventDispatcher, From 1abf35c8d7e742c345367e6ed565ca84cee8ba92 Mon Sep 17 00:00:00 2001 From: ernolf Date: Fri, 12 Jul 2024 22:04:33 +0200 Subject: [PATCH 5/6] Update return types to string in config methods Signed-off-by: ernolf --- lib/Service/Totp.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Service/Totp.php b/lib/Service/Totp.php index 1a9186284..cd00f535e 100644 --- a/lib/Service/Totp.php +++ b/lib/Service/Totp.php @@ -79,7 +79,7 @@ public function __construct(TotpSecretMapper $secretMapper, } private function getSecretLength(): int { - $length = (int)$this->config->getAppValue(Application::APP_ID, 'secret_length', self::DEFAULT_SECRET_LENGTH); + $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; } @@ -89,7 +89,7 @@ private function getHashAlgorithm(): string { } private function getTokenLength(): int { - $length = (int)$this->config->getAppValue(Application::APP_ID, 'token_length', self::DEFAULT_TOKEN_LENGTH); + $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; } From 8251d0f137ecd66e98e81bd195bb318f685bd96c Mon Sep 17 00:00:00 2001 From: ernolf Date: Sat, 13 Jul 2024 09:12:30 +0200 Subject: [PATCH 6/6] incorporate sugested changes from riview Signed-off-by: ernolf --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1222141d5..971ecae78 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,13 @@ 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: +### Configurable Options 1. **Secret Length:** * Default: 32 characters (160 bits) @@ -71,16 +70,14 @@ Since the OTP only grants access if exactly the same hash algorithm and the same occ config:app:set --value=9 -- twofactor_totp token_length ``` ---- -### Considerations: +### 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)