Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions best-practices/MASTG-BEST-0x01.md
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.
48 changes: 48 additions & 0 deletions demos/android/MASVS-CRYPTO/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md
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
---
Comment thread
cpholguera marked this conversation as resolved.

## 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.
51 changes: 51 additions & 0 deletions demos/android/MASVS-CRYPTO/MASTG-DEMO-0x01/MastgTest.kt
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()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The code hashes the string readme.txt, not the content of the file. This is not in line with what the documentation / test evaluation says.

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")
}
}
}
86 changes: 86 additions & 0 deletions demos/android/MASVS-CRYPTO/MASTG-DEMO-0x01/MastgTest_reversed.java
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();
}
}
19 changes: 19 additions & 0 deletions demos/android/MASVS-CRYPTO/MASTG-DEMO-0x01/output.txt
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");

1 change: 1 addition & 0 deletions demos/android/MASVS-CRYPTO/MASTG-DEMO-0x01/run.sh
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
11 changes: 11 additions & 0 deletions knowledge/android/MASVS-CRYPTO/MASTG-KNOW-0x01.md
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`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.
39 changes: 39 additions & 0 deletions rules/mastg-android-broken-hashing-algorithms.yaml
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)"?$
Loading