Skip to content

Add optional secret decryptor for Google Authenticator and TOTP - #328

Open
saukif wants to merge 1 commit into
scheb:8.xfrom
saukif:feature/secret-decryptor
Open

saukif wants to merge 1 commit into
scheb:8.xfrom
saukif:feature/secret-decryptor

Conversation

@saukif

@saukif saukif commented Oct 30, 2026

Copy link
Copy Markdown

Description

Some applications want to store the Google Authenticator or TOTP secret encrypted in the database, so that a database leak does not also leak the secrets. Today the bundle reads the secret straight from the user object, so there is no clean way to decrypt it first. The only option is to decorate the authenticator service and wrap the user object. This was discussed in #150, where you suggested an interface plus an optional service in the configuration. This PR does that.

What it adds:

  • A new interface, SecretDecryptorInterface, with one method: decrypt(string $secret): string.
  • A new option secret_decryptor for google and for totp. It takes the id of a service that implements the interface:
scheb_two_factor:
    google:
        secret_decryptor: acme.secret_decryptor
    totp:
        secret_decryptor: acme.secret_decryptor
  • GoogleTotpFactory and TotpFactory call the decryptor before they create the TOTP object. Checking a code, the QR code content and the validator constraints all go through these factories, so they all get the plain secret.

Backwards compatibility: the option is null by default. The decryptor is a new optional, last constructor argument of both factories. Without the option, nothing changes.

Encrypting the secret is still the job of the application, because the application stores it. generateSecret() returns a plain secret, as before. The Google and TOTP docs have a new section explaining this.

One open question: should decrypt() also get the user object, e.g. decrypt(string $secret, object $user)? That would help applications that use a different key per user or per customer. I kept it to the secret only to keep it simple, but it is easy to change before this is merged.

…cheb#150

Lets an application store the authenticator secret encrypted. The new
secret_decryptor option names a service implementing SecretDecryptorInterface,
which the TOTP factories call before using the secret. Without the option,
nothing changes.
@scheb

scheb commented Oct 30, 2026

Copy link
Copy Markdown
Owner

Thanks for working on this and for the detailled explanation.

I'm not fully convinced this should be handled within the bundle. In my opinion, such encryption logic is better placed at the persistence/ORM layer, where data is read from and written to the database. This bundle's responsibility should be to process the secret, though it should not need to know how that value was stored.

There are great solutions to address this problem on the data model layer:

Given these solutions are solving the problem with a bit of configuration, I'd rather prefer one of these, than adding an abstraction layer to the bundle that needs to be maintained, and requires users to implement custom code.

I'd be open to add to the documentation to mention this use case and point users towards ORM-level encrypted fields as the recommended approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants