Repository navigation
Add Android counterpart to MASTG-TEST-0211 for broken hashing algorithms #3952
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
0d1df84
7949fa3
3e8f7d9
21df7dc
5fa412b
b4e2ab7
b3fd991
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| --- | ||
| title: Use Strong Hash Functions for Security-Relevant Data | ||
| alias: use-strong-hash-functions-android | ||
| id: MASTG-BEST-0x01 | ||
| platform: android | ||
| knowledge: [MASTG-KNOW-0x01] | ||
| --- | ||
|
|
||
| Use a current hash function whenever collision resistance or cryptographic integrity matters. Android's [cryptography guidance](https://developer.android.com/privacy-and-security/cryptography) recommends the SHA-2 family, such as SHA-256, for `MessageDigest`. Do not use MD5 or SHA-1 for security-relevant hashing; Android classifies both as [weak or broken cryptographic hash functions](https://developer.android.com/privacy-and-security/risks/broken-cryptographic-algorithm). | ||
|
|
||
| A plain message digest does not authenticate its input. Use an HMAC with a SHA-2 family hash when integrity must be verified with a shared secret, or a digital signature when verification uses a public key. | ||
|
|
||
| Do not use a fast general-purpose hash such as SHA-256 directly for password storage. Use a salted password hashing function with an appropriate work factor, following the [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html). | ||
|
|
||
| For non-security checksums, document that collision resistance and authenticity are not required. This context lets reviewers distinguish an ordinary compatibility checksum from a digest used as a security control. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| --- | ||
| platform: android | ||
| title: Uses of Broken Hashing Algorithms in MessageDigest with semgrep | ||
| id: MASTG-DEMO-0x01 | ||
| code: [kotlin] | ||
| test: MASTG-TEST-0x01 | ||
| --- | ||
|
|
||
| ## Environment | ||
|
|
||
| - Mobile platform: Android 13 (API level 33) | ||
| - IDE: Android Studio 2026.1 | ||
| - Device: Pixel 7 Android Virtual Device with the Google APIs ARM64 system image | ||
|
|
||
| ## Sample | ||
|
|
||
| The code snippet below shows sample code that hashes data with MD5, SHA-1 (including the `SHA1` alias), and SHA-256. | ||
|
|
||
| {{ MastgTest.kt # MastgTest_reversed.java }} | ||
|
|
||
| ## Steps | ||
|
|
||
| Let's run our @MASTG-TOOL-0110 rule against the sample code. | ||
|
|
||
| {{ ../../../../rules/mastg-android-broken-hashing-algorithms.yaml }} | ||
|
|
||
| {{ run.sh }} | ||
|
|
||
| ## Observation | ||
|
|
||
| The rule has identified several instances in the code file where broken hashing algorithms are used. The specified line numbers can be located in the reverse-engineered code for further investigation and remediation. | ||
|
|
||
| {{ output.txt }} | ||
|
|
||
| ## Evaluation | ||
|
|
||
| The test case fails because the app hashes security-relevant data using broken algorithms, specifically MD5 and SHA-1. | ||
|
|
||
| Review each of the reported instances: | ||
|
|
||
| - Line 43 (FAIL): Hashes a password with MD5. In addition to being broken for collision resistance, raw MD5 is vulnerable to rapid offline dictionary and rainbow table attacks. | ||
| - Line 50 (FAIL): Hashes authentication token material with SHA-1. | ||
| - Line 57 (FAIL): Hashes authentication token material with the `SHA1` JCA alias of SHA-1. This is the same algorithm as line 50; the alias is included because Android code often uses `SHA1` instead of `SHA-1`. | ||
| - Line 64 (Requires Contextual Validation): Hashes `readme.txt` using MD5 for a checksum. While flagged by static analysis, using MD5 for non-security checksums (where collision resistance is not a security requirement) is generally acceptable; contextual validation is required rather than treating it as an unconditional failure. | ||
|
|
||
| Note that line 71 did not trigger the rule because the hash is generated using SHA-256 on ordinary file data (`readme.txt`), which passes this algorithm check. Note that while raw SHA-256 is appropriate for data integrity and hashing non-sensitive inputs, it is [unsuitable for password storage](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) (which requires salted, memory-hard key derivation functions such as Argon2id, scrypt, or PBKDF2). | ||
|
|
||
| See @MASTG-TEST-0x01 for more information. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package org.owasp.mastestapp | ||
|
|
||
| import android.content.Context | ||
| import android.util.Log | ||
| import java.security.MessageDigest | ||
|
|
||
| class MastgTest(private val context: Context) { | ||
|
|
||
| // SUMMARY: This sample demonstrates various common ways of hashing data with broken algorithms (MD5, SHA-1) via MessageDigest, plus a SHA-256 pass case. | ||
|
|
||
| private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) } | ||
|
|
||
| fun mastgTest(): String { | ||
| val password = "superSecretPassword!" | ||
| val tokenMaterial = "session-token-user-42" | ||
| val fileContents = "readme.txt" | ||
|
|
||
| // FAIL: [MASTG-TEST-0x01] The app hashes a password with MD5. | ||
| val md5 = MessageDigest.getInstance("MD5") | ||
| val md5Password = md5.digest(password.toByteArray(Charsets.UTF_8)).toHex() | ||
| Log.d("MASTG-TEST", "MD5 password hash: $md5Password") | ||
|
|
||
| // FAIL: [MASTG-TEST-0x01] The app hashes authentication token material with SHA-1. | ||
| val sha1 = MessageDigest.getInstance("SHA-1") | ||
| val sha1Token = sha1.digest(tokenMaterial.toByteArray(Charsets.UTF_8)).toHex() | ||
| Log.d("MASTG-TEST", "SHA-1 token hash: $sha1Token") | ||
|
|
||
| // FAIL: [MASTG-TEST-0x01] The app hashes authentication token material with the SHA1 JCA alias of SHA-1. | ||
| val sha1Alias = MessageDigest.getInstance("SHA1") | ||
| val sha1AliasToken = sha1Alias.digest(tokenMaterial.toByteArray(Charsets.UTF_8)).toHex() | ||
| Log.d("MASTG-TEST", "SHA1 alias token hash: $sha1AliasToken") | ||
|
|
||
| // CONTEXTUAL: [MASTG-TEST-0x01] The app uses MD5 for a checksum. Flagged by static analysis, but requires contextual validation to determine whether it is used in a security-relevant context. | ||
| val md5ChecksumDigest = MessageDigest.getInstance("MD5") | ||
| val md5Checksum = md5ChecksumDigest.digest(fileContents.toByteArray(Charsets.UTF_8)).toHex() | ||
| Log.d("MASTG-TEST", "MD5 checksum: $md5Checksum") | ||
|
|
||
| // PASS: [MASTG-TEST-0x01] The app hashes non-sensitive file/document data with SHA-256. | ||
| val sha256 = MessageDigest.getInstance("SHA-256") | ||
| val sha256Document = sha256.digest(fileContents.toByteArray(Charsets.UTF_8)).toHex() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code hashes the string |
||
| Log.d("MASTG-TEST", "SHA-256 document hash: $sha256Document") | ||
|
|
||
| return buildString { | ||
| appendLine("MD5 password hash: $md5Password") | ||
| appendLine("SHA-1 token hash: $sha1Token") | ||
| appendLine("SHA1 alias token hash: $sha1AliasToken") | ||
| appendLine("MD5 checksum: $md5Checksum") | ||
| appendLine("SHA-256 document hash: $sha256Document") | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| package org.owasp.mastestapp; | ||
|
|
||
| import android.content.Context; | ||
| import android.util.Log; | ||
| import java.security.MessageDigest; | ||
| import java.security.NoSuchAlgorithmException; | ||
| import java.util.Arrays; | ||
| import kotlin.Metadata; | ||
| import kotlin.collections.ArraysKt; | ||
| import kotlin.jvm.functions.Function1; | ||
| import kotlin.jvm.internal.Intrinsics; | ||
| import kotlin.text.Charsets; | ||
|
|
||
| /* JADX INFO: compiled from: MastgTest.kt */ | ||
| /* JADX INFO: loaded from: classes3.dex */ | ||
| @Metadata(d1 = {"\u0000\u001e\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0002\u0010\u0012\n\u0002\b\u0002\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\f\u0010\u0006\u001a\u00020\u0007*\u00020\bH\u0002J\u0006\u0010\t\u001a\u00020\u0007R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\n"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "<init>", "(Landroid/content/Context;)V", "toHex", "", "", "mastgTest", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) | ||
| public final class MastgTest { | ||
| public static final int $stable = 8; | ||
| private final Context context; | ||
|
|
||
| public MastgTest(Context context) { | ||
| Intrinsics.checkNotNullParameter(context, "context"); | ||
| this.context = context; | ||
| } | ||
|
|
||
| private final String toHex(byte[] $this$toHex) { | ||
| return ArraysKt.joinToString$default($this$toHex, (CharSequence) "", (CharSequence) null, (CharSequence) null, 0, (CharSequence) null, new Function1() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda0 | ||
| @Override // kotlin.jvm.functions.Function1 | ||
| public final Object invoke(Object obj) { | ||
| return MastgTest.toHex$lambda$0(((Byte) obj).byteValue()); | ||
| } | ||
| }, 30, (Object) null); | ||
| } | ||
|
|
||
| /* JADX INFO: Access modifiers changed from: private */ | ||
| public static final CharSequence toHex$lambda$0(byte it) { | ||
| String str = String.format("%02x", Arrays.copyOf(new Object[]{Byte.valueOf(it)}, 1)); | ||
| Intrinsics.checkNotNullExpressionValue(str, "format(...)"); | ||
| return str; | ||
| } | ||
|
|
||
| public final String mastgTest() throws NoSuchAlgorithmException { | ||
| MessageDigest md5 = MessageDigest.getInstance("MD5"); | ||
| byte[] bytes = "superSecretPassword!".getBytes(Charsets.UTF_8); | ||
| Intrinsics.checkNotNullExpressionValue(bytes, "getBytes(...)"); | ||
| byte[] bArrDigest = md5.digest(bytes); | ||
| Intrinsics.checkNotNullExpressionValue(bArrDigest, "digest(...)"); | ||
| String md5Password = toHex(bArrDigest); | ||
| Log.d("MASTG-TEST", "MD5 password hash: " + md5Password); | ||
| MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); | ||
| byte[] bytes2 = "session-token-user-42".getBytes(Charsets.UTF_8); | ||
| Intrinsics.checkNotNullExpressionValue(bytes2, "getBytes(...)"); | ||
| byte[] bArrDigest2 = sha1.digest(bytes2); | ||
| Intrinsics.checkNotNullExpressionValue(bArrDigest2, "digest(...)"); | ||
| String sha1Token = toHex(bArrDigest2); | ||
| Log.d("MASTG-TEST", "SHA-1 token hash: " + sha1Token); | ||
| MessageDigest sha1Alias = MessageDigest.getInstance("SHA1"); | ||
| byte[] bytes3 = "session-token-user-42".getBytes(Charsets.UTF_8); | ||
| Intrinsics.checkNotNullExpressionValue(bytes3, "getBytes(...)"); | ||
| byte[] bArrDigest3 = sha1Alias.digest(bytes3); | ||
| Intrinsics.checkNotNullExpressionValue(bArrDigest3, "digest(...)"); | ||
| String sha1AliasToken = toHex(bArrDigest3); | ||
| Log.d("MASTG-TEST", "SHA1 alias token hash: " + sha1AliasToken); | ||
| MessageDigest md5ChecksumDigest = MessageDigest.getInstance("MD5"); | ||
| byte[] bytes4 = "readme.txt".getBytes(Charsets.UTF_8); | ||
| Intrinsics.checkNotNullExpressionValue(bytes4, "getBytes(...)"); | ||
| byte[] bArrDigest4 = md5ChecksumDigest.digest(bytes4); | ||
| Intrinsics.checkNotNullExpressionValue(bArrDigest4, "digest(...)"); | ||
| String md5Checksum = toHex(bArrDigest4); | ||
| Log.d("MASTG-TEST", "MD5 checksum: " + md5Checksum); | ||
| MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); | ||
| byte[] bytes5 = "readme.txt".getBytes(Charsets.UTF_8); | ||
| Intrinsics.checkNotNullExpressionValue(bytes5, "getBytes(...)"); | ||
| byte[] bArrDigest5 = sha256.digest(bytes5); | ||
| Intrinsics.checkNotNullExpressionValue(bArrDigest5, "digest(...)"); | ||
| String sha256Document = toHex(bArrDigest5); | ||
| Log.d("MASTG-TEST", "SHA-256 document hash: " + sha256Document); | ||
| StringBuilder $this$mastgTest_u24lambda_u241 = new StringBuilder(); | ||
| $this$mastgTest_u24lambda_u241.append("MD5 password hash: " + md5Password).append('\n'); | ||
| $this$mastgTest_u24lambda_u241.append("SHA-1 token hash: " + sha1Token).append('\n'); | ||
| $this$mastgTest_u24lambda_u241.append("SHA1 alias token hash: " + sha1AliasToken).append('\n'); | ||
| $this$mastgTest_u24lambda_u241.append("MD5 checksum: " + md5Checksum).append('\n'); | ||
| $this$mastgTest_u24lambda_u241.append("SHA-256 document hash: " + sha256Document).append('\n'); | ||
| return $this$mastgTest_u24lambda_u241.toString(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
|
|
||
|
|
||
| ┌─────────────────┐ | ||
| │ 4 Code Findings │ | ||
| └─────────────────┘ | ||
|
|
||
| MastgTest_reversed.java | ||
| ❯❱ rules.mastg-android-broken-hashing-algorithms | ||
| ❰❰ Blocking ❱❱ | ||
| [MASVS-CRYPTO-1] Broken hashing algorithms found in use. | ||
|
|
||
| 43┆ MessageDigest md5 = MessageDigest.getInstance("MD5"); | ||
| ⋮┆---------------------------------------- | ||
| 50┆ MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); | ||
| ⋮┆---------------------------------------- | ||
| 57┆ MessageDigest sha1Alias = MessageDigest.getInstance("SHA1"); | ||
| ⋮┆---------------------------------------- | ||
| 64┆ MessageDigest md5ChecksumDigest = MessageDigest.getInstance("MD5"); | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| NO_COLOR=true semgrep -c ../../../../rules/mastg-android-broken-hashing-algorithms.yaml ./MastgTest_reversed.java > output.txt |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| masvs_category: MASVS-CRYPTO | ||
| platform: android | ||
| title: Message Digests in the Java Cryptography Architecture | ||
| --- | ||
|
|
||
| Android exposes hash functions through the Java Cryptography Architecture (JCA) [`MessageDigest`](https://developer.android.com/reference/java/security/MessageDigest) class. Applications select an algorithm with `MessageDigest.getInstance`. Overloads either use the first registered provider that implements the requested algorithm or select a provider by name or object. | ||
|
|
||
| Android's `MessageDigest` implementation supports MD5, SHA-1, and the SHA-2 family, including SHA-224, SHA-256, SHA-384, and SHA-512. Algorithm names and aliases are defined by the Java Security Standard Algorithm Names specification. The selected instance exposes its canonical algorithm name and provider through `getAlgorithm` and `getProvider`. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't appear to be true: getAlgorithm returns whatever you specified in getInstance; not the canonical name. |
||
|
|
||
| A message digest processes input with `update` and completes the computation with `digest`. The result is a fixed-length byte array whose size depends on the selected algorithm. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| rules: | ||
| - id: mastg-android-broken-hashing-algorithms | ||
| languages: | ||
| - java | ||
| severity: WARNING | ||
| metadata: | ||
| summary: This rule looks for broken hashing algorithms in MessageDigest.getInstance calls and common third-party wrappers. | ||
| message: "[MASVS-CRYPTO-1] Broken hashing algorithms found in use." | ||
| pattern-either: | ||
| - patterns: | ||
| - pattern-either: | ||
| - pattern: MessageDigest.getInstance($ALGO, ...) | ||
| - pattern: java.security.MessageDigest.getInstance($ALGO, ...) | ||
| - metavariable-regex: | ||
| metavariable: $ALGO | ||
| regex: (?i)^"?(MD2|MD4|MD5|SHA-1|SHA1|SHA)"?$ | ||
| - pattern: Hashing.md5(...) | ||
| - pattern: com.google.common.hash.Hashing.md5(...) | ||
| - pattern: Hashing.sha1(...) | ||
| - pattern: com.google.common.hash.Hashing.sha1(...) | ||
| - pattern: DigestUtils.md5(...) | ||
| - pattern: org.apache.commons.codec.digest.DigestUtils.md5(...) | ||
| - pattern: DigestUtils.md5Hex(...) | ||
| - pattern: org.apache.commons.codec.digest.DigestUtils.md5Hex(...) | ||
| - pattern: DigestUtils.sha1(...) | ||
| - pattern: org.apache.commons.codec.digest.DigestUtils.sha1(...) | ||
| - pattern: DigestUtils.sha1Hex(...) | ||
| - pattern: org.apache.commons.codec.digest.DigestUtils.sha1Hex(...) | ||
| - pattern: DigestUtils.getMd5Digest(...) | ||
| - pattern: org.apache.commons.codec.digest.DigestUtils.getMd5Digest(...) | ||
| - pattern: DigestUtils.getSha1Digest(...) | ||
| - pattern: org.apache.commons.codec.digest.DigestUtils.getSha1Digest(...) | ||
| - patterns: | ||
| - pattern-either: | ||
| - pattern: DigestUtils.getDigest($ALGO, ...) | ||
| - pattern: org.apache.commons.codec.digest.DigestUtils.getDigest($ALGO, ...) | ||
| - metavariable-regex: | ||
| metavariable: $ALGO | ||
| regex: (?i)^"?(MD2|MD4|MD5|SHA-1|SHA1|SHA)"?$ |
Uh oh!
There was an error while loading. Please reload this page.