Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add configurable options #1546

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* 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.
* 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.

I think "synchronized" should be phrazed a bit clearer. Can users actually change/upgrade their TOTP app entries when the admin changes these settings?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can users actually change/upgrade their TOTP app entries when the admin changes these settings?

Yes. If the token length and/or the hash algorithm are changed on the server, existing secrets can continue to be used as soon as these values ​​are set to the same in the TOTP app.
This opens up the possibility of a completely new security strategy. For example, by agreeing on alternating token lengths or hash algorithms according to a predetermined time frame.
Yes, I know, that sounds a bit exaggerated, but that definitely makes Nextcloud's TOTP future proof.
Unfortunately, I cannot contribute any screenshots of my Aegis app because the app prevents screenshots for security reasons.

How would you like to explain it more clearly?

An admin will certainly first try to figure out how it works and quickly find out.

But I'm always in favor of a better explanation, but I couldn't think of anything better right now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. If the token length and/or the hash algorithm are changed on the server, existing secrets can continue to be used as soon as these values ​​are set to the same in the TOTP app.

I use FreeOTP and it does not allow me to change this.

How about we persist the algorithm and token length with the secret? Then an admin can change the default, but they will only affect new registrations, old ones will just continue to work?

Copy link
Author

@ernolf ernolf Jul 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we persist the algorithm and token length with the secret? Then an admin can change the default, but they will only affect new registrations, old ones will just continue to work?

I think that would be very difficult to implement.
Of course everything is possible but then different instances of the app would have to run alongside each other. Basically for every registered TOTP user and then it wouldn't be a second factor app but a beast
Or can you think of an elegant way to implement such?

I would like to encourage you to install Aegis and migrate your existing secrets. Then you can try it out by yourself and see how it feels.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

different instances of the app would have to run alongside each other.

Well, I just realize that this is nonsense what I wrote.. it won't be that bad, but data will have to be stored about which user registered with which algorithm/token length.
I can imagine that, but the implementation is certainly beyond my coding skills

Copy link
Author

@ernolf ernolf Jul 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The time window of 30 seconds can also be changed, both with the nextcloud twofactor_totp app as with Aegis. However, after a lot of trying, I was unable to create effective, working OTPs with a time window other than 30 seconds. That is why I did not implement that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data will have to be stored about which user registered with which algorithm/token length.

Which would open up completely new possibilities, where every user can set and change their TOTP values ​​themselves.
Changes should only be applied if an OTP with the new settings has been correctly generated, as a sign and counter-proof that it works (as with the first setup).
If you can already see how this could be implemented, then that is of course all the better and then this PR can be closed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to encourage you to install Aegis and migrate your existing secrets. Then you can try it out by yourself and see how it feels.

Fair, but not everyone uses this app so I would like to find a way that is fool proof ;-)

it won't be that bad, but data will have to be stored about which user registered with which algorithm/token length.

We have the table twofactor_totp_secrets, which we can amend by columns for algorithm etc. At registration time we persist the options used, and continue to use those even when the admins have set new default options (with better security.

To achieve this

  1. Add a migration to add the columns
  2. Adjust \OCA\TwoFactorTOTP\Db\TotpSecretMapper and \OCA\TwoFactorTOTP\Db\TotpSecret for the new new attributes (mapped from database rows)
  3. Write options when a secret is generated
  4. Read options when a secret is used

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ChristophWurst
Please take a look at the new PR #1549 which addresses your security concerns, we should continue to work on that one and close this PR if it is okay for you.


* 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)

Expand All @@ -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)

40 changes: 37 additions & 3 deletions lib/Service/Totp.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,30 @@

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;
use OCA\TwoFactorTOTP\Event\StateChanged;
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;
Expand All @@ -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 {
Expand All @@ -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');
}

/**
Expand Down Expand Up @@ -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();
Expand Down
Loading