Conversation
Replace the unsalted MD5 AES key for TOTP secrets with PBKDF2 matching scratch-token hashing, and re-encrypt legacy rows inside ValidateAndConsumeTOTP. Assisted-by: Composer
Encode salt as pbkdf2$<salt>$<ciphertext> in the existing column so legacy base64 secrets need no schema change. Assisted-by: Composer
|
This needs more work, it does not actually fix the advisory. Working on it. |
Every value encrypted with SECRET_KEY used an unstretched hash of it as the AES key, md5 for TOTP secrets and sha256 for webhook auth headers, Actions secrets, LDAP bind passwords and migration credentials. Any one of them let a weak SECRET_KEY be brute-forced quickly from a database dump, so stretching only TOTP secrets did not help. Derive a single Argon2id key once per process for all of them, mark new ciphertexts with a "v2:" prefix and re-encrypt existing rows in a migration instead of on login. Legacy values stay readable as a fallback for rows the migration could not convert, such as already queued migration tasks. Assisted-by: Claude Code:claude-opus-5-5
|
Current fix works, but has a migration, I'm checking if that can be avoided. |
TOTP secrets are never queued, so the only remaining reason for a runtime md5 fallback was a migration run under a wrong SECRET_KEY. Drop it to keep the legacy format confined to migration 356 and trim the tests accordingly. Assisted-by: Claude Code:claude-opus-5-5
|
Diff is now as small as it can get. There is no solution without the migration because weak encrypted values must be rewritten in the database, or else someone with a database dump could break the weak crypto in it. |
The migration targets the next release, whose migrations live in v29. Assisted-by: Claude Code:claude-opus-5-5
The v2 format already forces every value to be re-encrypted, so switch it from unauthenticated AES-CFB to AES-256-GCM now. Tampered values and a wrong SECRET_KEY then fail cleanly instead of decrypting to garbage. Also trim the migration and its tests. Assisted-by: Claude Code:claude-opus-5-5
If migration 356 runs under a wrong SECRET_KEY, it leaves TOTP rows in the legacy format. Without a runtime fallback those stayed unreadable even after restoring the right key. Share one md5 fallback between the runtime and the migration. Assisted-by: Claude Code:claude-opus-5-5
|
Since it is a large migration, can we introduce the preparation for the key rotation? Move all encrypted contents into a single table and manage them together. Then, to rotate, only need to update that table.
Pros: easy to rotate the master key (IIRC there was a discussion in discord, cc @TheFox0x7 ) I am neutral for the decisions, so this comment is just a question or suggestion, no blocker. |
|
Sounds doable. I think I will also drop this MD5 fallback after all to keep the codebase clean from legacy mechanisms that could trigger security scanners needlessly. |
I haven't thought about the problem carefully, so not sure whether we should do that (single encrypt content table) or not. It really depends on how Gitea will manage the encrypted content in the future. If we'd like to do more, we need a careful and complete design first. Or, just use the current good enough approach, leave more discussions to the future. |
|
A quick idea: without introducing a single encrypted content table, maybe we can move Then, we can introduce a cli sub-command to call This seems a minimal change, won't make anything more complicated, and should be good enough for the future. |
|
I redid my research into that (I'll recheck with previous looks when I'll get a chance) and I think the one common table would not be a good approach. For it to work properly you need FKs to lock deletion if someone holds a reference and while it's nice and simple to migrate there's not much use in it apart from that - unless you'd be planning to enable multiple references... so a user could define a secret once and reuse that somewhere else from a dropdown or such. Plain columns are probably simpler to use and maintain - no joins, no table, no possibly broken references if something goes wrong. It's what grafana and gitlab use so I'd assume it's a sane pick for this. Downside being the spread between tables but it's not like this changes much. |
|
Does this also handle this case: #16832 ? |
Agree, I proposed |
By the way, I think this PR doesn't really help the situation. The current situation is: most instances are using the default SECRET_KEY. So, even if you use a derived key to re-encrypt the values, the derived key is still the same for most instances (no need to brute-force) |
Fixes https://github.com/go-gitea/gitea/security/advisories/GHSA-673g-8f9j-6q25 (supersedes #36966). Every value encrypted with
SECRET_KEYused an unstretched hash of it as the AES key, md5 for TOTP secrets and sha256 for webhook auth headers, Actions secrets, LDAP bind passwords and migration credentials. Any one of them allowed fast offline brute-force of a weakSECRET_KEY, so hardening only TOTP secrets would not help.SECRET_KEYonce per processv2:EncryptSecret/DecryptSecretSECRET_KEY