Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,50 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [7.6.0] - 2026-05-07

- Android SDK version: 18.3.0
- iOS SDK version: 6.14.4

### Breaking
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 need to raise major because of this (on all platforms)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Bumped to 8.0.0


- `SuspiciousAppInfo.reason` (String) renamed to `reasons` (List\<String\>)
- Value `"blacklist"` in `reasons` renamed to `"blocklist"`

### Flutter

#### Deprecated

- `blacklistedPackageNames`, `blacklistedHashes`, `suspiciousPermissions`, `whitelistedInstallationSources` are deprecated but remain functional — use `SuspiciousAppDetectionConfig` instead

### Android

#### Added

- Added a new sub-check for `HMA` detection to the root detector
- Added a new sub-check for `KernelSU` detection to the root detector
- Added a new sub-check for `Frida Server` detection to the hook detector
- Added Huawei App Market provider to HMA detection queries
- New API class `SuspiciousAppDetectionConfig` that can be used to configure malware detection
- New API for malware detection configuration in `TalsecConfig`, see `TalsecConfig.Builder#suspiciousAppDetection`

#### Fixed

- Fixed `VerifyError` caused by `JaCoCo` bytecode instrumentation
- Fixed a potential cause of crash in the multi-instance detector
- Fixed crash caused by unhandled `SecurityException` thrown by `UsageStatsManager` in root detection
- Fixed manifest merge conflicts in HMA detection providers
- Fixed Java interoperability of `ScreenProtector` methods
- Fixed Kotlin classpath conflicts in SDK dependency resolution (Kotlin 2.0.0)

#### Changed

- Fine-tuned `KernelSU` detection
- Fine-tuned hook detection
- Fine-tuned location spoofing detection
- Modified malware incident log structure for better aggregation
- Old malware configuration API methods in `TalsecConfig.Builder` tagged as deprecated (but remain functional): `blacklistedPackageNames`, `blacklistedHashes`, `suspiciousPermissions`, `whitelistedInstallationSources`

## [7.5.1] - 2026-03-24

- Android SDK version: 18.0.4
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version '1.0-SNAPSHOT'

buildscript {
ext.kotlin_version = '2.1.0'
ext.talsec_version = '18.0.4'
ext.talsec_version = '18.3.0'
repositories {
google()
mavenCentral()
Expand Down
46 changes: 45 additions & 1 deletion android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import android.content.Context
import android.content.pm.PackageInfo
import android.os.Build
import com.aheaditec.talsec_security.security.api.ExternalIdResult
import com.aheaditec.talsec_security.security.api.MalwareScanScope
import com.aheaditec.talsec_security.security.api.ReasonMode
import com.aheaditec.talsec_security.security.api.ScopeType
import com.aheaditec.talsec_security.security.api.SuspiciousAppDetectionConfig
import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo
import io.flutter.plugin.common.MethodChannel
import org.json.JSONObject
import com.aheaditec.freerasp.generated.PackageInfo as FlutterPackageInfo
import com.aheaditec.freerasp.generated.SuspiciousAppInfo as FlutterSuspiciousAppInfo

Expand Down Expand Up @@ -33,7 +38,7 @@ internal inline fun runResultCatching(result: MethodChannel.Result, block: () ->
* this [SuspiciousAppInfo].
*/
internal fun SuspiciousAppInfo.toPigeon(context: Context): FlutterSuspiciousAppInfo {
return FlutterSuspiciousAppInfo(this.packageInfo.toPigeon(context), this.reason)
return FlutterSuspiciousAppInfo(this.packageInfo.toPigeon(context), this.reasons.toList())
}

/**
Expand Down Expand Up @@ -83,3 +88,42 @@ internal fun ExternalIdResult.resolve(result: MethodChannel.Result) {
is ExternalIdResult.Error -> result.error("external-id-failure", this.errorMsg, null)
}
}

internal fun JSONObject.toMalwareScanScope(): MalwareScanScope {
val scopeTypeStr = optString("scanScope", "SIDELOADED_ONLY")
val scanScope = runCatching { ScopeType.valueOf(scopeTypeStr) }.getOrDefault(ScopeType.SIDELOADED_ONLY)
val trustedInstallSources = optJSONArray("trustedInstallSources")?.let { arr ->
(0 until arr.length()).map { arr.getString(it) }
}
return MalwareScanScope(scanScope, trustedInstallSources)
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.

SIDELOADED_ONLY is hardcoded as a string in optString, then again as an enum in getOrDefault, then again as a fallback object — three places, two languages, no compile-time guarantee they stay in sync.

The Dart side makes scanScope required (so it can't be missing on the wire) but the Kotlin side still defends against it being absent with a string default. We need only one source of truth.

Suggested approach: just throw exception here if scope is missing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Refactored

}

internal fun JSONObject.toSuspiciousAppDetectionConfig(): SuspiciousAppDetectionConfig {
val packageNames = optJSONArray("packageNames")?.let { arr ->
(0 until arr.length()).map { arr.getString(it) }.toSet()
}
val hashes = optJSONArray("hashes")?.let { arr ->
(0 until arr.length()).map { arr.getString(it) }.toSet()
}
val requestedPermissions = optJSONArray("requestedPermissions")?.let { outer ->
(0 until outer.length()).map { i ->
val inner = outer.getJSONArray(i)
(0 until inner.length()).map { j -> inner.getString(j) }.toSet()
}.toSet()
}
val grantedPermissions = optJSONArray("grantedPermissions")?.let { outer ->
(0 until outer.length()).map { i ->
val inner = outer.getJSONArray(i)
(0 until inner.length()).map { j -> inner.getString(j) }.toSet()
}.toSet()
}
val malwareScanScope = optJSONObject("malwareScanScope")?.toMalwareScanScope()
?: MalwareScanScope(ScopeType.SIDELOADED_ONLY, emptyList())
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.

Do not fallback to default args - it creates non-debuggable issues. Just throw exception

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Refactored

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.

The single-level and nested patterns each repeat a lot. The file already has a perfectly good generic helper for this — Utils.kt defines extractArray() and processArray() with reified types.

Can you try to reuse those ?

Also, mapTo(mutableSetOf()) avoids building a List and then copying to a Set.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Refactored

val reasonModeStr = optString("reasonMode")
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.

org.json.JSONObject#optString(name) returns "" (empty string) for a missing key, not null. The next line correctly uses isNullOrEmpty() so it works, but the variable name reasonModeStr plus the isNullOrEmpty() check imply the value can be null — which it can't from this API. Slightly misleading and inconsistent with the optString("scanScope", "SIDELOADED_ONLY") pattern used 30 lines above, which uses the two-arg overload to provide a default.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Refactored

val reasonMode = if (reasonModeStr.isNullOrEmpty()) {
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.

Can we set default in Dart to ReasonMode.HIGHEST_CONFIDENCE, make the argument required in kotlin and throw exception if missing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Refactored

ReasonMode.HIGHEST_CONFIDENCE
} else {
runCatching { ReasonMode.valueOf(reasonModeStr) }.getOrDefault(ReasonMode.HIGHEST_CONFIDENCE)
}
return SuspiciousAppDetectionConfig(packageNames, hashes, requestedPermissions, grantedPermissions, malwareScanScope, reasonMode)
}
11 changes: 8 additions & 3 deletions android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ internal object Utils {
val alternativeStores = androidConfig.extractArray<String>("supportedStores")
val malwareConfig = parseMalwareConfig(androidConfig)

return TalsecConfig.Builder(packageName, certificateHashes)
val builder = TalsecConfig.Builder(packageName, certificateHashes)
.watcherMail(watcherMail)
.supportedAlternativeStores(alternativeStores)
.prod(isProd)
Expand All @@ -47,7 +47,12 @@ internal object Utils {
.blacklistedHashes(malwareConfig.blacklistedHashes)
.suspiciousPermissions(malwareConfig.suspiciousPermissions)
.whitelistedInstallationSources(malwareConfig.whitelistedInstallationSources)
.build()

androidConfig.optJSONObject("suspiciousAppDetectionConfig")?.let {
builder.suspiciousAppDetection(it.toSuspiciousAppDetectionConfig())
}

return builder.build()
}

private fun parseMalwareConfig(androidConfig: JSONObject): MalwareConfig {
Expand Down Expand Up @@ -175,4 +180,4 @@ private inline fun <reified T> processArray(jsonArray: JSONArray): Array<T> {
}

return list.toTypedArray()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,20 @@ data class PackageInfo (
/** Generated class from Pigeon that represents data sent in messages. */
data class SuspiciousAppInfo (
val packageInfo: PackageInfo,
val reason: String
val reasons: List<String>
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): SuspiciousAppInfo {
val packageInfo = pigeonVar_list[0] as PackageInfo
val reason = pigeonVar_list[1] as String
return SuspiciousAppInfo(packageInfo, reason)
val reasons = pigeonVar_list[1] as List<String>
return SuspiciousAppInfo(packageInfo, reasons)
}
}
fun toList(): List<Any?> {
return listOf(
packageInfo,
reason,
reasons,
)
}
}
Expand Down
10 changes: 7 additions & 3 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,16 @@ Future<void> _initializeTalsec() async {
packageName: 'com.aheaditec.freeraspExample',
signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='],
supportedStores: ['com.sec.android.app.samsungapps'],
malwareConfig: MalwareConfig(
blacklistedPackageNames: ['com.aheaditec.freeraspExample'],
suspiciousPermissions: [
suspiciousAppDetectionConfig: const SuspiciousAppDetectionConfig(
packageNames: ['com.aheaditec.freeraspExample'],
requestedPermissions: [
['android.permission.CAMERA'],
['android.permission.READ_SMS', 'android.permission.READ_CONTACTS'],
],
malwareScanScope: MalwareScanScope(
scanScope: ScopeType.sideloadedOnly,
),
reasonMode: ReasonMode.highestConfidence,
),
),
iosConfig: IOSConfig(
Expand Down
2 changes: 1 addition & 1 deletion example/lib/widgets/malware_bottom_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class MalwareListTile extends StatelessWidget {

return ListTile(
title: Text(malware.packageInfo.packageName),
subtitle: Text('Reason: ${malware.reason}'),
subtitle: Text('Reasons: ${malware.reasons.join(', ')}'),
leading: appIcon,
);
},
Expand Down
8 changes: 4 additions & 4 deletions lib/src/generated/talsec_pigeon_api.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,25 +63,25 @@ class PackageInfo {
class SuspiciousAppInfo {
SuspiciousAppInfo({
required this.packageInfo,
required this.reason,
required this.reasons,
});

PackageInfo packageInfo;

String reason;
List<String> reasons;

Object encode() {
return <Object?>[
packageInfo,
reason,
reasons,
];
}

static SuspiciousAppInfo decode(Object result) {
result as List<Object?>;
return SuspiciousAppInfo(
packageInfo: result[0]! as PackageInfo,
reason: result[1]! as String,
reasons: (result[1] as List<Object?>?)!.cast<String>(),
);
}
}
Expand Down
7 changes: 6 additions & 1 deletion lib/src/models/android_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ class AndroidConfig {
required this.packageName,
required this.signingCertHashes,
this.supportedStores = const [],
this.malwareConfig,
@Deprecated('Use suspiciousAppDetectionConfig instead') this.malwareConfig,
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.

Keeping both APIs is going to be huge pain. What if I specify both in config? which is going to be used?

Let's remove the old API altogether (on all platforms)

We are doing new major here so we can do this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed completely

this.suspiciousAppDetectionConfig,
}) {
ConfigVerifier.verifyAndroid(this);
}
Expand All @@ -34,5 +35,9 @@ class AndroidConfig {
final List<String> supportedStores;

/// Malware configuration for Android.
@Deprecated('Use suspiciousAppDetectionConfig instead')
final MalwareConfig? malwareConfig;

/// Suspicious app detection configuration for Android.
final SuspiciousAppDetectionConfig? suspiciousAppDetectionConfig;
}
10 changes: 9 additions & 1 deletion lib/src/models/android_config.g.dart
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.

Did you generate this using pigeon? Or are those manual edits?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Manual edit 😅

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions lib/src/models/malware_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ part 'malware_config.g.dart';
class MalwareConfig {
/// Creates a new instance of [MalwareConfig].
MalwareConfig({
@Deprecated('Use SuspiciousAppDetectionConfig instead')
this.blacklistedPackageNames = const [],
@Deprecated('Use SuspiciousAppDetectionConfig instead')
this.blacklistedHashes = const [],
@Deprecated('Use SuspiciousAppDetectionConfig instead')
this.suspiciousPermissions = const [],
@Deprecated('Use SuspiciousAppDetectionConfig instead')
this.whitelistedInstallationSources = const [],
});

Expand All @@ -21,14 +25,18 @@ class MalwareConfig {
Map<String, dynamic> toJson() => _$MalwareConfigToJson(this);

/// List of blocklisted applications with given package name.
@Deprecated('Use SuspiciousAppDetectionConfig instead')
final List<String> blacklistedPackageNames;

/// List of blocklisted applications with given hash.
@Deprecated('Use SuspiciousAppDetectionConfig instead')
final List<String> blacklistedHashes;

/// List of blocklisted applications with given permissions.
@Deprecated('Use SuspiciousAppDetectionConfig instead')
final List<List<String>> suspiciousPermissions;

/// List of whitelisted installation sources.
@Deprecated('Use SuspiciousAppDetectionConfig instead')
final List<String> whitelistedInstallationSources;
}
1 change: 1 addition & 0 deletions lib/src/models/models.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export 'android_config.dart';
export 'ios_config.dart';
export 'malware_config.dart';
export 'suspicious_app_detection_config.dart';
export 'talsec_config.dart';
Loading