Skip to content

Use pbkdf2 instead of md5, add salt in totp - #36966

Closed
lunny wants to merge 3 commits into
go-gitea:mainfrom
lunny:lunny/totp_algorithm
Closed

lunny wants to merge 3 commits into
go-gitea:mainfrom
lunny:lunny/totp_algorithm

Conversation

@lunny

@lunny lunny commented Mar 23, 2026 •

Copy link
Copy Markdown
Member

Updated the two-factor secret storage to use PBKDF2-derived AES keys with per-user salts, added a secret_algo marker and migration (including backfilling existing rows to md5), and implemented on-login re-encryption of legacy secrets. Added migration tests for the new columns and unit tests covering PBKDF2 setup and legacy secret upgrade behavior.


Generated by Coding Agent with Codex 5.2

@GiteaBot GiteaBot added the lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. label Mar 23, 2026
@lunny
lunny requested a review from Copilot March 23, 2026 20:52

Copilot AI left a comment

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.

Pull request overview

This PR strengthens TOTP secret protection by introducing per-user salting and PBKDF2-derived encryption keys, while keeping backward compatibility by upgrading legacy (md5-derived) encrypted secrets on successful validation.

Changes:

  • Add secret_salt / secret_algo fields to TwoFactor and a migration (v330) to add columns and mark existing rows as legacy (md5).
  • Update ValidateTOTP to support legacy decryption and opportunistically re-encrypt secrets with PBKDF2+salt (returning an upgraded flag).
  • Update auth flows/tests to use the new (ok, upgraded, err) return signature and cover upgrade behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
services/auth/basic.go Persists upgraded 2FA secret on successful OTP validation for basic auth flow.
routers/web/auth/password.go Updates TOTP validation call signature in password reset flow.
routers/web/auth/2fa.go Updates TOTP validation call signature in web 2FA sign-in flow.
models/auth/twofactor.go Adds salt/algo fields; switches key derivation to PBKDF2; implements legacy upgrade on validate.
models/auth/twofactor_test.go Adds tests for PBKDF2 secrets and legacy upgrade behavior.
models/migrations/v1_26/v330.go Adds migration to introduce secret_salt / secret_algo columns and set legacy algo.
models/migrations/v1_26/v330_test.go Adds migration test asserting new columns exist.
models/migrations/migrations.go Registers migration 330.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread models/auth/twofactor.go
Comment thread models/auth/twofactor.go
Comment on lines +151 to +156
return ok, false, err
}
key, _ = t.getEncryptionKey()
secretBytes, err = secret.AesEncrypt(key, []byte(secretStr))
if err != nil {
return ok, false, err

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

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

When rotateSecretSalt() fails during a legacy-secret upgrade, the function returns ok (which is true in this branch) alongside a non-nil error. Returning false for ok when err != nil avoids ambiguous results and prevents future callers from accidentally accepting a passcode if they mishandle the error.

Suggested change
return ok, false, err
}
key, _ = t.getEncryptionKey()
secretBytes, err = secret.AesEncrypt(key, []byte(secretStr))
if err != nil {
return ok, false, err
return false, false, err
}
key, _ = t.getEncryptionKey()
secretBytes, err = secret.AesEncrypt(key, []byte(secretStr))
if err != nil {
return false, false, err

Copilot uses AI. Check for mistakes.
Comment thread models/auth/twofactor.go
Comment on lines +151 to +156
return ok, false, err
}
key, _ = t.getEncryptionKey()
secretBytes, err = secret.AesEncrypt(key, []byte(secretStr))
if err != nil {
return ok, false, err

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

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

Similarly, if re-encryption of the legacy secret fails, this returns ok (true) with a non-nil error. Prefer returning false for ok whenever err is non-nil to keep the API contract consistent and reduce the risk of misuse by future callers.

Suggested change
return ok, false, err
}
key, _ = t.getEncryptionKey()
secretBytes, err = secret.AesEncrypt(key, []byte(secretStr))
if err != nil {
return ok, false, err
return false, false, err
}
key, _ = t.getEncryptionKey()
secretBytes, err = secret.AesEncrypt(key, []byte(secretStr))
if err != nil {
return false, false, err

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com>
@silverwind

silverwind commented Mar 24, 2026 •

Copy link
Copy Markdown
Member

PBKDF2-derived AES keys

How "secure" is this? Never heard of PBKDF2 personally. Maybe consider argon2:

image

@wxiaoguang wxiaoguang left a comment •

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.

The code reads very strange.

There are many calls to ValidateTOTP, but why only one call does UpdateTwoFactor ?

Shouldn't ValidateTOTP fully handle the "upgrade"?

The test is also very strange, it never really tested the data is upgraded in database, never tested that after upgrade the TOTP can still succeed by reading from database.

And the check t.SecretAlgo == "" || t.SecretAlgo == "md5" || t.SecretSalt == "" is also very strange, why not just simply treat "empty" as legacy "md5"? Why need to use string const "md5"?


Conclusion: AI slop

@GiteaBot GiteaBot added lgtm/blocked A maintainer has reservations with the PR and thus it cannot be merged and removed lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. labels Mar 24, 2026
@bircni

bircni commented Oct 1, 2026

Copy link
Copy Markdown
Member

Superseded by #39540 — rewritten on current main with upgrade handled inside ValidateAndConsumeTOTP, empty secret_salt as the legacy marker (no secret_algo/md5 string), and DB round-trip coverage.

Assisted-by: Composer

@wxiaoguang wxiaoguang closed this Oct 2, 2026
@lunny
lunny deleted the lunny/totp_algorithm branch October 3, 2026 04:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm/blocked A maintainer has reservations with the PR and thus it cannot be merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants