-
Notifications
You must be signed in to change notification settings - Fork 156
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
Encrypt TOTP secrets. #389
base: master
Are you sure you want to change the base?
Conversation
d28fcac
to
6c380c8
Compare
Hmm... This will need a composer install to make it work in Travis CI. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I support this in concept, and the code looks good to me from a quick skim. I'm leaving a preliminary approval until I can have the time to give it a thorough test -- I'm just occupied with Application Passwords stuff at the moment with Beta 1 about to go out for 5.6
This relies on the constant "SECURE_AUTH_SALT" what if it ever changes? The decrypt will not work, especially if not backup of the old wp-config is found. |
To clarify, maybe this isn't a problem with Two-factor, But I came here from the Application Password intergration Guide And @georgestephanis refered here as a way to store application password. In this way it can cause problems. |
Using a different constant is fine. I didn't want to muck with WP Core for this PR. |
@georgestephanis any thoughts on a review of this PR and whether it's ready for consideration in v0.8.0? |
Given the 0.8.0 focus on U2F deprecation, I'm going to punt this to a future release. |
@soatok I'd abstracted out the encryption methods into the parent class to make them easier for other providers to adopt as well -- any concerns on that front from your end? I renamed a couple of them slightly to yoink the explicit totp references there. Also -- with sodium_compat in wp core, what's the point of adding it to composer here? Is it more like ... safety compat for old versions of core or is it still necessary for the travis_ci? |
No concerns with what's implemented. Other providers may want to add domain separation versus yours, so we might want to expand
It's mostly just to ensure the build passes. If it gets it from WP, that's sufficient, and it can be removed. |
What is the reasoning behind the HMAC ? Just use libsodium to generate a random key on activation and write it to wp-config. wp_salt must not be used here as you will never be able to rotate them again |
I wasn't confident in my ability to write to wp-config as I'm not a WordPress developer. |
It might not be possible to write to it on some hosts. That should not compromise the security of the majority. But regardless, hashing the key does not seem to serve a purpose. Unless Im missing something |
OK, well, if you or someone else wants to tackle the wp-config stuff, I'd prefer that over messing with salts.
It does two things:
But either way, it'd be better to replace that with a wp-config directive. |
Any reason not to use https://paragonie.com/book/pecl-libsodium/read/04-secretkey-crypto.md ? |
Secretbox is secure encryption, but not AEAD. AEAD lets you authenticate both some encrypted data and additional data. This lets you bind an encrypted value to some context and prevent confused deputy attacks. |
Yes, the only additional data that is relevant here is the user_id right? This means an attacker with write access to the DB can't swap a user's TOTP secret with one from another user I guess. |
WordPress raising the minimum PHP version would be great. I wouldn't count on it happening just for 2FA.
Which part of sodium_compat? How will you handle key rotation? |
Using wp-paseto as a proof of concept, this is how you would protect against Confused Deputy Attacks while supporting key rotation. <?php
$keyring = new PasetoKeyManager(array(
'key-id-1' => sodium_hex2bin(/* hex encode your key here */),
'key-id-2' => sodium_hex2bin(/* hex encode your key here */),
// ...
'key-id-65535' => sodium_hex2bin(/* hex encode your key here */)
));
$encrypted = $keyring->encrypt(
$user_totp_ecret,
'key-id-65535',
'totp-secret-for-user' . $user_id
); As long as there exists a definition that maps Additionally, the user ID is used (with a hard-coded prefix) as an "implicit assertion" (authenticated data that is not stored in the footer). This gives you a defense against confused deputy attacks. Feel free to implement the necessary bits yourself elsewhere. |
c30cd36
to
e7cd00a
Compare
Actually, why do we need encryption with AD here. Assuming an attacker has WRITE access to the database he could just delete the TOTP settings for the target user in which case the plugin will not require TOTP anyway |
What if we add a code configuration that enforces TOTP for all users and thus removes that avenue for attack if an attacker only gets access to the SQL database? AAD is useful for preventing Confused Deputy attacks. If you're going to encrypt, you should always use the AAD mechanism to bind ciphertexts to a given context, even if you don't think you need it. |
Yes, that was my idea also and I think it's the only way do to this. IE, after all admins of the site configured 2FA, write a constant to the config that makes it mandatory to have 2FA preconfigured on login for admins. |
We've tried to tackle this outside the plugin (https://github.com/WordPress/wporg-two-factor/blob/trunk/wporg-two-factor.php#L78-L130), it's very much dependant on the site config, and/or the site owners preference as to how far you go with capabilities, since there isn't anything stopping you changing an lower level account to have full access via capabilities. |
This has gotten very long, so I think a summary will help organize things. GoalTo protect against an attacker who has read access to database, but not write. if they can read the plain TOTP seed then they can generate valid tokens. If they have write access, though, there's nothing we can do. Different approaches
Miscellaneous notes
|
Based on the summary above, I think these are the decisions we need to make:
|
@iandunn ping me again here pls if I don't comment within the next days |
I believe that WP.com uses
Seems like being separate provides more flexibility, and probably less issues if/when it gets updated. For creating it, I guess we need to consider read-only file systems, or systems where you can't directly write to files that are controlled by the host - not sure the best way to solve this though. In an ideal world, the constant would be version controlled by the site itself, so perhaps having the ability to set this outside of the plugin is desirable? |
IMO, the best balance of security and practicality is:
Rotating can be sussed out in #456 , but I posted a comment there with my rough idea. |
@calvinalkan ping 🙂 |
I'm not sure using a salt for encryption is wise. My understanding of all the salt values stored in WP is that they're designed so they can be changed at any time. This can be useful in instances where everyone needs to be logged out at once. Change the auth salts and cookies are invalidated. By using the salts to encrypt, there are two potential issues:
|
I do agree, that basing it off The initial value of the option could be derived from a salt on the site at the time of activation however, as long as no other random/time-based seeds are added to it. |
Encrypt with AD.
If possible don't implement ourselves. If PHP5 should be supported I highly recommend using defuse which supports 5.6. Otherwise, Halite is the de-facto libsodium wrapper in PHP. (Active (GitHub issues) support only for PHP8+) Halite uses:
AD could be the user id or username since both SHOULD be immutable.
From most ideal to least ideal:
declare(strict_types=1);
use LogicException;
use function file_get_contents;
use function is_string;
use function trim;
final class SecretStore
{
// This is an abstraction for defined()
private ConstantStore $constant_store;
/**
* @var callable(string): ?string
*/
private $fetch_secret_custom;
/**
* @var array<non-empty-string, non-empty-string>
*/
private array $cache = [];
/**
* @param ?callable(string): ?string $fallback
*/
public function __construct(ConstantStore $constant_store, ?callable $fallback = null)
{
$this->constant_store = $constant_store;
$this->fetch_secret_custom = $fallback ?: static fn () => null;
}
/**
* @param non-empty-string $name
*
* @return non-empty-string
*
* @see https://github.com/symphonycms/symphonycms/issues/2569 for reasons to prefer $_SERVER over getenv
*/
public function get(string $name): string
{
$value = $this->cache[$name] ?? null;
if (null !== $value) {
return $value;
}
if (isset($_SERVER[$name])) {
/** @var mixed $server */
$server = $_SERVER[$name];
$file_contents = @file_get_contents((string) $server);
/** @var mixed $value */
$value = (false !== $file_contents)
? trim($file_contents)
: $server;
} elseif ($this->constant_store->exists($name)) {
$value = $this->constant_store->getString($name);
} else {
$value = ($this->fetch_secret_custom)($name);
}
if (! is_string($value) || '' === $value) {
throw new LogicException("The value for secret [{$name}] must be a non-empty-string.");
}
$this->cache[$name] = $value;
return $value;
}
}
TOTP only. |
This can be any callable that implements the fallback behavior, i.e write to wp-config, use wp-salt, whatever |
Since the discussion in WordPress#389 seems to be on-going, the following should allow users of the plugin to decide how they would like to approach the encryption part. Perhaps we can follow up with solutions after they've been implemented and debate further?
This exposes a versioned authenticated encryption interface (powered by libsodium, which is polyfilled in WordPress via sodium_compat).
Encryption is opportunistic: Unencrypted TOTP secrets will automatically be upgraded to an encrypted secret upon retrieval or update of their TOTP secret.
Version 1 of the under-the-hood protocol uses the HMAC-SHA256 hash of a constant string and SECURE_AUTH_SALT to generate a key. This can be changed safely in version 2 without breaking old users.
Version 1 uses XChaCha20-Poly1305 to encrypt the TOTP secrets. The authentication tag on the ciphertext is also validated over the user's database ID and the version prefix.
Threat model
Cryptography Primitives