Add encrypted TOTP secret field to accounts#135
Conversation
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
3f9dca0 to
3ca19e0
Compare
| ]; | ||
|
|
||
| $tab[] = [ | ||
| 'id' => '30', |
There was a problem hiding this comment.
id 30 already used (please change to 31)
There was a problem hiding this comment.
Fixed, changed to 31. Good catch, thanks!
| $aeskey = new AesKey(); | ||
| if ($hash_id | ||
| && $aeskey->getFromDBByCrit(['plugin_accounts_hashes_id' => $hash_id]) | ||
| && !empty($aeskey->fields['name'])) { |
There was a problem hiding this comment.
And if i haven't stored aeskey into database, i cannot encrypt it ?
There was a problem hiding this comment.
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=""> |
There was a problem hiding this comment.
How can i see the uncrypted value of TOTP secret ?
There was a problem hiding this comment.
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 callsdecrypt_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.
- 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.
9ebbd45 to
bad9cc9
Compare
- 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.
Summary
encrypted_totp_secretcolumn to store TOTP (Time-based One-Time Password) seeds alongside account credentialsThis 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
install/sql/update-3.3.0.sqlencrypted_totp_secretcolumninstall/sql/empty-3.2.1.sqlhook.php$DB->fieldExists()guardsrc/Account.phpencryptTotpSecret()helper,rawSearchOptions,prepareInputForAdd/Update,showFormsrc/Hash.phpupdateHash()now re-encrypts TOTP secret alongside passwordtemplates/account.html.twigDesign Decisions
nosearch+nodisplayon the search option: encrypted content should never appear in search results or list views.Test Plan
encrypted_totp_secretcolumn exists inglpi_plugin_accounts_accountsFixes #127, closes #96, closes #102