Skip to content

chore: [ANDROSDK-2059] replace MD5 password hashing with PBKDF2 - #2703

Open
taridepaco wants to merge 3 commits into
developfrom
ANDROSDK-2059
Open

chore: [ANDROSDK-2059] replace MD5 password hashing with PBKDF2#2703
taridepaco wants to merge 3 commits into
developfrom
ANDROSDK-2059

Conversation

@taridepaco

@taridepaco taridepaco commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

UserHelper.md5() is no longer used to store password hashes. Credentials are now hashed with PBKDF2-HMAC-SHA256 (210,000 iterations, 16-byte random salt) through a new internal PasswordHasher, and Credentials.getHash() is replaced by newPasswordHash() for the write path and matches() for the read path, so a salted hash is never compared for equality.

Existing accounts migrate transparently rather than being logged out. Hashes are stored in a self-describing $pbkdf2-sha256$i=<iterations>$<salt>$<hash> format: legacy MD5 values are still verified (verify-only, never produced again) and are rewritten in place on the next successful login, while the plaintext secret is still at hand. The same mechanism allows raising the iteration count in a later release without invalidating anything already stored. UserHelper.md5 is kept as @Deprecated because it is published API and the legacy verification path still needs it; removal is left for the next major. Note that PBKDF2WithHmacSHA256 is only available from API 26, so devices below that fall back to PBKDF2WithHmacSHA1, which is recorded in the stored value and upgraded when the account is next verified on a capable device.

Related task: ANDROSDK-2059

Resolve the overlap between the PBKDF2 password hash migration and the login
refactor landed on develop:

- loginOnline writes a fresh PBKDF2 hash when the credentials carry a secret and
  otherwise preserves the stored one, keeping develop's fix that stops a token
  re-login from wiping the PIN hash.
- verifyPinAgainstStoredHash is gone: develop moved the PIN check for token
  accounts into tryLoginOffline, which already verifies the hash.
- tryLoginOffline verifies instead of comparing and upgrades an outdated hash in
  place, reporting failures through develop's wrongLocalCredentialsError.
- Tests adopt develop's givenExistingDatabase helper and stub the current hash
  format; the legacy MD5 path keeps its own dedicated tests.
@sonarqubecloud

sonarqubecloud Bot commented Aug 24, 2026

Copy link
Copy Markdown

* The result is salted and therefore different on every call, so it must never be compared:
* use [matches] to verify a secret against an already stored hash.
*/
fun newPasswordHash(): String? {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would avoid adding the new word in a method name because after some versions it won't be new anymore.
Why don't keep the same name as before? This method is replacing it.

fun matches(storedHash: String?): HashVerification {
val secret = passwordOrPin
return when {
secret == null && storedHash == null -> HashVerification.Match(needsUpgrade = false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In case the existing hash was null, I am not sure if we should let the user in by providing a null value. If the hash is null, it means that the user didn't set up a PIN code.

deriveKey(parsed.algorithm, secret, parsed.salt, parsed.iterations)
} catch (_: NoSuchAlgorithmException) {
// The hash was written on a device running a newer Android version. It cannot be
// verified here, so the account has to be authenticated online again.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We would have to check if this restriction is important when exporting/importing a DB from a newer Android version. In version 3.6 we should move to use a different password for DB encryption and then force the user the do an online login in case they want to synchronize. I think it won't be so important if we implement this workflow, but it might be important if we keep the current one.

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