Skip to content

Add encrypted TOTP secret field to accounts#135

Closed
RPGMais wants to merge 5 commits into
InfotelGLPI:masterfrom
RPGMais:feature/totp-secret-field
Closed

Add encrypted TOTP secret field to accounts#135
RPGMais wants to merge 5 commits into
InfotelGLPI:masterfrom
RPGMais:feature/totp-secret-field

Conversation

@RPGMais

@RPGMais RPGMais commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a dedicated encrypted_totp_secret column to store TOTP (Time-based One-Time Password) seeds alongside account credentials
  • TOTP secret is encrypted server-side using the same AES-256-CTR fingerprint as the password
  • Show/hide toggle in the UI follows the same UX pattern as the password field
  • Re-encryption is handled automatically when changing the fingerprint key

This is a highly requested feature (3 separate issues) that allows users to securely store 2FA seeds used by authenticators like Google Authenticator, Authy, and Bitwarden, instead of storing them as plaintext in the "Others" or "Comments" fields.

Changes

File Change
install/sql/update-3.3.0.sql Migration: adds encrypted_totp_secret column
install/sql/empty-3.2.1.sql Updated for fresh installations
hook.php Migration block with $DB->fieldExists() guard
src/Account.php encryptTotpSecret() helper, rawSearchOptions, prepareInputForAdd/Update, showForm
src/Hash.php updateHash() now re-encrypts TOTP secret alongside password
templates/account.html.twig TOTP input field with show/hide toggle and "Clear" checkbox

Design Decisions

  • Server-side encryption (not client-side like the password): TOTP seeds are short strings (16-32 chars base32) submitted via standard form POST. Client-side encryption overhead is unnecessary and would add JS complexity.
  • No live TOTP code generator: Kept out of this PR to minimize scope. Can be added as a follow-up with a lightweight JS library.
  • nosearch + nodisplay on the search option: encrypted content should never appear in search results or list views.

Test Plan

  • Fresh install: verify encrypted_totp_secret column exists in glpi_plugin_accounts_accounts
  • Upgrade from 3.2.x: verify migration adds the column without errors
  • Create account with TOTP secret: verify it's stored encrypted in DB
  • Edit account: verify "Leave blank to keep current" placeholder, existing secret preserved
  • Clear TOTP: check "Clear" checkbox, save, verify secret is removed
  • Change fingerprint key: verify TOTP secret is re-encrypted alongside password
  • Show/hide toggle: verify eye icon toggles input visibility

Fixes #127, closes #96, closes #102

Add support for storing TOTP (Time-based One-Time Password) secrets
alongside account credentials. The TOTP secret is encrypted server-side
using the same fingerprint as the password, providing secure storage
for 2FA seeds used by authenticators like Google Authenticator, Authy,
and Bitwarden.

Changes:
- New `encrypted_totp_secret` column (TEXT) in accounts table
- Server-side encryption via AccountCrypto (same AES-256-CTR as passwords)
- Show/hide toggle in the UI (same UX pattern as the password field)
- "Clear" checkbox to remove stored TOTP secret
- Re-encryption support when changing the fingerprint key (Hash::updateHash)
- Migration file for existing installations (update-3.3.0.sql)

The TOTP secret is encrypted server-side (unlike the password which is
encrypted client-side in JS). This is intentional as TOTP seeds are
short strings submitted via standard form POST and don't benefit from
the client-side encryption overhead.

Fixes InfotelGLPI#127
Closes InfotelGLPI#96
Closes InfotelGLPI#102
@RPGMais RPGMais force-pushed the feature/totp-secret-field branch from 3f9dca0 to 3ca19e0 Compare March 24, 2026 17:42
Comment thread src/Account.php Outdated
];

$tab[] = [
'id' => '30',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

id 30 already used (please change to 31)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed, changed to 31. Good catch, thanks!

Comment thread src/Account.php
$aeskey = new AesKey();
if ($hash_id
&& $aeskey->getFromDBByCrit(['plugin_accounts_hashes_id' => $hash_id])
&& !empty($aeskey->fields['name'])) {

@tsmr tsmr Apr 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

And if i haven't stored aeskey into database, i cannot encrypt it ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point. In the previous version I had a plaintext fallback, but after reviewing it I realized that storing the TOTP secret unencrypted in encrypted_totp_secret would be misleading and a security risk (the value is exposed in the HTML source for client-side decryption, same as the password).

So now if there's no AES key stored in the database, the TOTP secret is not saved and the user gets an error message: "TOTP secret not saved: no encryption key available."

The encryption happens server-side in prepareInputForAdd/Update using AccountCrypto::encrypt() with the stored fingerprint. The decryption is done client-side via JS (same pattern as the password) so the user can view it after entering the encryption key or when auto-decrypt is enabled.

class="form-control"
autocomplete="off"
placeholder="{{ item.isNewItem() ? '' : __('Leave blank to keep current', 'accounts') }}"
value="">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How can i see the uncrypted value of TOTP secret ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a "Stored TOTP Secret" field next to the input, with the same reveal (eye icon on mousedown) and copy-to-clipboard buttons that the password uses.

The decryption works the same way as the password:

  • If the AES key is stored in the DB, auto_decrypt() now also calls decrypt_totp_secret() automatically on page load
  • If not, the user enters the encryption key manually and clicks the unlock button -- both password and TOTP are decrypted together

The decrypt_totp_secret() function was added to account.form.js.php and handles both v2 and legacy encryption formats.

RPGMais added 2 commits May 1, 2026 17:24
- Change searchOption ID from 30 to 31 (30 already in use)
- Store TOTP secret as plaintext fallback when no AES key is configured
- Add "Stored TOTP Secret" field with decrypt/reveal/copy buttons
  (same UX pattern as the password field)
- Add decrypt_totp_secret() JS function, called alongside password
  decryption in auto_decrypt() and uncryptpassword()
- Add missing account.form.js from upstream master
The plugin loads account.form.js.php (PHP wrapper), not account.form.js.
Add decrypt_totp_secret() to the correct file and remove the unused one.
@RPGMais RPGMais force-pushed the feature/totp-secret-field branch from 9ebbd45 to bad9cc9 Compare May 1, 2026 20:37
RPGMais added 2 commits May 1, 2026 17:52
- Reject TOTP save when no AES key is configured (instead of plaintext
  fallback that would expose the secret in HTML source)
- Re-encrypt TOTP secret during entity transfer (prevents data loss
  when accounts are moved between entities with different fingerprints)
- Add addslashes() to encryptTotpSecret for consistency with updateHash
….form.js)

Upstream moved from account.form.js.php (PHP wrapper) to account.form.js
(plain JS using CFG_GLPI.root_doc). Accept the deletion of .js.php and
apply TOTP decrypt changes to the new .js file.
@tsmr

tsmr commented May 2, 2026

Copy link
Copy Markdown
Contributor

5990de7

@tsmr tsmr closed this May 2, 2026
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.

[Enhancement] Add encrypted TOTP secret field to accounts OTP TOTP Support

2 participants