Initial commit
This commit is contained in:
62
core/security/build.gradle.kts
Normal file
62
core/security/build.gradle.kts
Normal file
@@ -0,0 +1,62 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("kotlin-kapt")
|
||||
id("dagger.hilt.android.plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.smoa.core.security"
|
||||
compileSdk = AppConfig.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = AppConfig.minSdk
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:common"))
|
||||
|
||||
implementation(platform(Dependencies.composeBom))
|
||||
implementation(Dependencies.composeUi)
|
||||
implementation(Dependencies.androidxCoreKtx)
|
||||
implementation(Dependencies.securityCrypto)
|
||||
implementation(Dependencies.okHttp)
|
||||
implementation(Dependencies.coroutinesCore)
|
||||
implementation(Dependencies.coroutinesAndroid)
|
||||
|
||||
implementation(Dependencies.hiltAndroid)
|
||||
kapt(Dependencies.hiltAndroidCompiler)
|
||||
|
||||
implementation(Dependencies.roomRuntime)
|
||||
implementation(Dependencies.roomKtx)
|
||||
kapt(Dependencies.roomCompiler)
|
||||
// SQLite support for SQLCipher
|
||||
implementation("androidx.sqlite:sqlite:2.4.0")
|
||||
|
||||
// Database Encryption
|
||||
implementation(Dependencies.sqlcipher)
|
||||
|
||||
// Testing
|
||||
testImplementation(Dependencies.junit)
|
||||
testImplementation(Dependencies.mockk)
|
||||
testImplementation(Dependencies.coroutinesTest)
|
||||
testImplementation(Dependencies.truth)
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package com.smoa.core.security
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Database
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Audit event types for security logging.
|
||||
*/
|
||||
enum class AuditEventType {
|
||||
AUTHENTICATION_SUCCESS,
|
||||
AUTHENTICATION_FAILURE,
|
||||
AUTHENTICATION_LOCKOUT,
|
||||
SESSION_START,
|
||||
SESSION_END,
|
||||
SESSION_TIMEOUT,
|
||||
CREDENTIAL_ACCESS,
|
||||
CREDENTIAL_DISPLAY,
|
||||
COMMUNICATION_SESSION_START,
|
||||
COMMUNICATION_SESSION_END,
|
||||
MEETING_JOIN,
|
||||
MEETING_JOINED,
|
||||
MEETING_LEFT,
|
||||
MEETING_CREATED,
|
||||
MEETING_HOST,
|
||||
POLICY_UPDATE,
|
||||
STEP_UP_AUTH_REQUIRED,
|
||||
STEP_UP_AUTH_SUCCESS,
|
||||
STEP_UP_AUTH_FAILURE,
|
||||
CHANNEL_JOINED,
|
||||
CHANNEL_LEFT,
|
||||
PTT_STARTED,
|
||||
PTT_STOPPED
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit log entry entity.
|
||||
*/
|
||||
@Entity(tableName = "audit_logs")
|
||||
data class AuditLogEntry(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
val timestamp: Date,
|
||||
val eventType: AuditEventType,
|
||||
val userId: String?,
|
||||
val module: String?,
|
||||
val details: String?,
|
||||
val ipAddress: String?,
|
||||
val deviceId: String?
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface AuditLogDao {
|
||||
@Query("SELECT * FROM audit_logs ORDER BY timestamp DESC LIMIT :limit")
|
||||
fun getRecentLogs(limit: Int): Flow<List<AuditLogEntry>>
|
||||
|
||||
@Insert
|
||||
suspend fun insertLog(entry: AuditLogEntry)
|
||||
|
||||
@Query("SELECT * FROM audit_logs WHERE timestamp >= :since ORDER BY timestamp DESC")
|
||||
suspend fun getLogsSince(since: Date): List<AuditLogEntry>
|
||||
|
||||
@Query("DELETE FROM audit_logs WHERE timestamp < :before")
|
||||
suspend fun deleteLogsBefore(before: Date)
|
||||
}
|
||||
|
||||
@Database(entities = [AuditLogEntry::class], version = 1, exportSchema = false)
|
||||
@TypeConverters(DateConverter::class)
|
||||
abstract class AuditLogDatabase : RoomDatabase() {
|
||||
abstract fun auditLogDao(): AuditLogDao
|
||||
}
|
||||
|
||||
/**
|
||||
* Date converter for Room database.
|
||||
*/
|
||||
class DateConverter {
|
||||
@TypeConverter
|
||||
fun fromTimestamp(value: Long?): Date? {
|
||||
return value?.let { Date(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun dateToTimestamp(date: Date?): Long? {
|
||||
return date?.time
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit Logger for security event logging.
|
||||
*/
|
||||
@Singleton
|
||||
class AuditLogger @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val encryptionManager: EncryptionManager
|
||||
) {
|
||||
private val database: AuditLogDatabase = Room.databaseBuilder(
|
||||
context,
|
||||
AuditLogDatabase::class.java,
|
||||
"audit_logs"
|
||||
)
|
||||
.enableMultiInstanceInvalidation()
|
||||
.build()
|
||||
|
||||
private val dao = database.auditLogDao()
|
||||
|
||||
/**
|
||||
* Log a security-relevant event.
|
||||
*/
|
||||
suspend fun logEvent(
|
||||
eventType: AuditEventType,
|
||||
userId: String? = null,
|
||||
module: String? = null,
|
||||
details: String? = null
|
||||
) {
|
||||
logEvent(eventType, emptyMap(), userId, module, details)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a security-relevant event with additional details map.
|
||||
*/
|
||||
suspend fun logEvent(
|
||||
eventType: AuditEventType,
|
||||
detailsMap: Map<String, String>,
|
||||
userId: String? = null,
|
||||
module: String? = null,
|
||||
details: String? = null
|
||||
) {
|
||||
val detailsString = if (detailsMap.isNotEmpty()) {
|
||||
val mapString = detailsMap.entries.joinToString(", ") { "${it.key}=${it.value}" }
|
||||
if (details != null) "$details | $mapString" else mapString
|
||||
} else {
|
||||
details
|
||||
}
|
||||
|
||||
val entry = AuditLogEntry(
|
||||
timestamp = Date(),
|
||||
eventType = eventType,
|
||||
userId = userId,
|
||||
module = module,
|
||||
details = detailsString,
|
||||
ipAddress = null, // Can be populated if network info available
|
||||
deviceId = android.provider.Settings.Secure.getString(
|
||||
context.contentResolver,
|
||||
android.provider.Settings.Secure.ANDROID_ID
|
||||
)
|
||||
)
|
||||
dao.insertLog(entry)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent audit logs.
|
||||
*/
|
||||
fun getRecentLogs(limit: Int = 100): Flow<List<AuditLogEntry>> {
|
||||
return dao.getRecentLogs(limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logs since a specific date (for sync).
|
||||
*/
|
||||
suspend fun getLogsSince(since: Date): List<AuditLogEntry> {
|
||||
return dao.getLogsSince(since)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old logs (retention policy).
|
||||
*/
|
||||
suspend fun cleanupOldLogs(retentionDays: Int = 90) {
|
||||
val cutoffDate = Date(System.currentTimeMillis() - (retentionDays * 24 * 60 * 60 * 1000L))
|
||||
dao.deleteLogsBefore(cutoffDate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Export logs for transmission (encrypted).
|
||||
*/
|
||||
suspend fun exportLogsForSync(since: Date): ByteArray {
|
||||
val logs = getLogsSince(since)
|
||||
// Serialize and encrypt logs before transmission
|
||||
// This is a placeholder - implement proper serialization and encryption
|
||||
return logs.toString().toByteArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance audit trail with immutable record support.
|
||||
* Creates cryptographically bound records that cannot be modified.
|
||||
*/
|
||||
suspend fun createImmutableRecord(entry: AuditLogEntry): AuditLogEntry {
|
||||
// In production, add cryptographic binding (hash chain, Merkle tree, etc.)
|
||||
// For now, return as-is - will be enhanced in Phase 1
|
||||
return entry
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind timestamp to audit record per eIDAS requirements.
|
||||
*/
|
||||
suspend fun bindTimestamp(entry: AuditLogEntry): AuditLogEntry {
|
||||
// Timestamp binding will be implemented with qualified timestamping service
|
||||
// Placeholder for Phase 3 eIDAS implementation
|
||||
return entry
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.smoa.core.security
|
||||
|
||||
import okhttp3.CertificatePinner
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class CertificatePinningManager @Inject constructor() {
|
||||
|
||||
/**
|
||||
* Create an OkHttpClient with certificate pinning enabled.
|
||||
* This ensures all network requests verify the server's certificate chain.
|
||||
*/
|
||||
fun createPinnedClient(
|
||||
hostname: String,
|
||||
pins: List<String>
|
||||
): OkHttpClient.Builder {
|
||||
val certificatePinner = CertificatePinner.Builder()
|
||||
.apply {
|
||||
pins.forEach { pin ->
|
||||
add(hostname, pin)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
return OkHttpClient.Builder()
|
||||
.certificatePinner(certificatePinner)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default pinned client for enterprise endpoints.
|
||||
* Pins should be configured per deployment.
|
||||
*/
|
||||
fun createEnterpriseClient(): OkHttpClient.Builder {
|
||||
// Placeholder - actual pins must be configured per deployment
|
||||
return OkHttpClient.Builder()
|
||||
.apply {
|
||||
// Certificate pinning will be configured via policy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.smoa.core.security
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import net.zetetic.database.sqlcipher.SupportOpenHelperFactory
|
||||
import javax.crypto.SecretKey
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Helper for creating encrypted Room databases using SQLCipher.
|
||||
* Binds encryption keys to user authentication state.
|
||||
*/
|
||||
@Singleton
|
||||
class EncryptedDatabaseHelper @Inject constructor(
|
||||
private val encryptionManager: EncryptionManager,
|
||||
private val keyManager: KeyManager
|
||||
) {
|
||||
companion object {
|
||||
private const val KEY_ALIAS_PREFIX = "db_encryption_key_"
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create encryption key for a database.
|
||||
* Keys are bound to device and user authentication state.
|
||||
*/
|
||||
fun getDatabaseKey(alias: String): ByteArray {
|
||||
// Get key from secure storage or generate new one
|
||||
val keyString = keyManager.getSecureString("$KEY_ALIAS_PREFIX$alias")
|
||||
|
||||
return if (keyString != null) {
|
||||
// Key exists, decode from base64
|
||||
android.util.Base64.decode(keyString, android.util.Base64.DEFAULT)
|
||||
} else {
|
||||
// Generate new key
|
||||
val key = encryptionManager.getOrCreateEncryptionKey(alias)
|
||||
val keyBytes = key.encoded
|
||||
|
||||
// Store key in secure storage (base64 encoded)
|
||||
val encodedKey = android.util.Base64.encodeToString(keyBytes, android.util.Base64.DEFAULT)
|
||||
keyManager.putSecureString("$KEY_ALIAS_PREFIX$alias", encodedKey)
|
||||
|
||||
keyBytes
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SQLCipher open helper factory for Room database.
|
||||
*/
|
||||
fun createOpenHelperFactory(databaseName: String): SupportSQLiteOpenHelper.Factory {
|
||||
val key = getDatabaseKey(databaseName)
|
||||
val passphrase = SupportOpenHelperFactory(key)
|
||||
|
||||
return passphrase
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database passphrase as String (SQLCipher requires String).
|
||||
*/
|
||||
fun getDatabasePassphrase(databaseName: String): String {
|
||||
val key = getDatabaseKey(databaseName)
|
||||
// Convert key bytes to String (SQLCipher requirement)
|
||||
// In production, consider using a more secure conversion
|
||||
return android.util.Base64.encodeToString(key, android.util.Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate database encryption key.
|
||||
* This should be called periodically or on security events.
|
||||
*/
|
||||
fun rotateDatabaseKey(databaseName: String): Result<Unit> {
|
||||
return try {
|
||||
// Remove old key
|
||||
keyManager.removeSecureString("$KEY_ALIAS_PREFIX$databaseName")
|
||||
|
||||
// Generate new key
|
||||
getDatabaseKey(databaseName)
|
||||
|
||||
kotlin.Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
kotlin.Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.smoa.core.security
|
||||
|
||||
import android.content.Context
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import androidx.security.crypto.EncryptedFile
|
||||
import androidx.security.crypto.MasterKey
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.io.File
|
||||
import java.security.KeyStore
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class EncryptionManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
private val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply {
|
||||
load(null)
|
||||
}
|
||||
|
||||
private val masterKey: MasterKey = MasterKey.Builder(context)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Get or create a hardware-backed encryption key for data at rest.
|
||||
* Keys are non-exportable and bound to the device.
|
||||
*/
|
||||
fun getOrCreateEncryptionKey(alias: String): javax.crypto.SecretKey {
|
||||
if (!keyStore.containsAlias(alias)) {
|
||||
val keyGenerator = KeyGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_AES,
|
||||
"AndroidKeyStore"
|
||||
)
|
||||
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
|
||||
alias,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
||||
)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.setKeySize(256)
|
||||
.setUserAuthenticationRequired(false) // Can be enabled for additional security
|
||||
.build()
|
||||
|
||||
keyGenerator.init(keyGenParameterSpec)
|
||||
keyGenerator.generateKey()
|
||||
}
|
||||
|
||||
return keyStore.getKey(alias, null) as javax.crypto.SecretKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an encrypted file for storing sensitive data.
|
||||
*/
|
||||
fun createEncryptedFile(fileName: String): EncryptedFile {
|
||||
val file = File(context.filesDir, fileName)
|
||||
return EncryptedFile.Builder(
|
||||
context,
|
||||
file,
|
||||
masterKey,
|
||||
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if hardware-backed keystore is available.
|
||||
*/
|
||||
fun isHardwareBacked(): Boolean {
|
||||
return try {
|
||||
val key = getOrCreateEncryptionKey("test_key_check")
|
||||
val factory = KeyStore.getInstance("AndroidKeyStore")
|
||||
factory.load(null)
|
||||
val entry = factory.getEntry("test_key_check", null) as? KeyStore.SecretKeyEntry
|
||||
entry?.let {
|
||||
val secretKey = it.secretKey
|
||||
// Note: AndroidKeyStoreSecretKey is not directly accessible in all API levels
|
||||
// This is a simplified check - in production, use KeyInfo for detailed key characteristics
|
||||
try {
|
||||
// Attempt to get key characteristics (API 23+)
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
factory.getKey("test_key_check", null)?.let {
|
||||
// Key exists and is accessible - assume hardware-backed for AndroidKeyStore
|
||||
true
|
||||
} ?: false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} ?: false
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.smoa.core.security
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class KeyManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val encryptionManager: EncryptionManager
|
||||
) {
|
||||
private val masterKey: MasterKey = MasterKey.Builder(context)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
private val encryptedPrefs: SharedPreferences = EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"secure_prefs",
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
|
||||
/**
|
||||
* Store a secure key/value pair. Keys are bound to device and user auth state.
|
||||
*/
|
||||
fun putSecureString(key: String, value: String) {
|
||||
encryptedPrefs.edit().putString(key, value).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a secure key/value pair.
|
||||
*/
|
||||
fun getSecureString(key: String, defaultValue: String? = null): String? {
|
||||
return encryptedPrefs.getString(key, defaultValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a secure key/value pair.
|
||||
*/
|
||||
fun removeSecureString(key: String) {
|
||||
encryptedPrefs.edit().remove(key).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all secure preferences.
|
||||
*/
|
||||
fun clearAll() {
|
||||
encryptedPrefs.edit().clear().apply()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.smoa.core.security
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.os.Build
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Screen protection utility to prevent screenshots and screen recording.
|
||||
* Implements FLAG_SECURE and media projection detection.
|
||||
*/
|
||||
@Singleton
|
||||
class ScreenProtection @Inject constructor(
|
||||
private val context: Context
|
||||
) {
|
||||
private val mediaProjectionManager: MediaProjectionManager? by lazy {
|
||||
context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as? MediaProjectionManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable screen protection for an activity.
|
||||
* This prevents screenshots and screen recording (where supported by OS).
|
||||
*/
|
||||
fun enableScreenProtection(activity: Activity) {
|
||||
activity.window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
|
||||
// Additional protection for Android 11+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
WindowCompat.setDecorFitsSystemWindows(activity.window, false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable screen protection for an activity.
|
||||
* Use with caution - only disable when absolutely necessary.
|
||||
*/
|
||||
fun disableScreenProtection(activity: Activity) {
|
||||
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if media projection (screen recording) is active.
|
||||
* Note: This is a best-effort check and may not detect all cases.
|
||||
*/
|
||||
fun isScreenRecordingActive(): Boolean {
|
||||
return try {
|
||||
// Check if media projection service is available and active
|
||||
// This is a simplified check - full implementation would require
|
||||
// monitoring media projection callbacks
|
||||
mediaProjectionManager != null
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable helper to enable screen protection for Compose screens.
|
||||
*/
|
||||
@Composable
|
||||
fun EnableScreenProtection() {
|
||||
val view = LocalView.current
|
||||
val activity = view.context as? Activity
|
||||
|
||||
activity?.let {
|
||||
enableScreenProtection(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.smoa.core.security
|
||||
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Advanced threat detection system.
|
||||
*/
|
||||
@Singleton
|
||||
class ThreatDetection @Inject constructor(
|
||||
private val auditLogger: AuditLogger
|
||||
) {
|
||||
|
||||
/**
|
||||
* Detect anomalies in user behavior.
|
||||
*/
|
||||
suspend fun detectAnomalies(userId: String, activity: UserActivity): Result<ThreatAssessment> {
|
||||
// TODO: Implement machine learning-based anomaly detection
|
||||
return Result.success(ThreatAssessment.NORMAL)
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze security events for threats.
|
||||
*/
|
||||
suspend fun analyzeSecurityEvents(events: List<SecurityEvent>): Result<ThreatReport> {
|
||||
// TODO: Implement threat analysis
|
||||
return Result.success(ThreatReport(emptyList(), ThreatLevel.LOW))
|
||||
}
|
||||
}
|
||||
|
||||
data class UserActivity(
|
||||
val userId: String,
|
||||
val timestamp: Date,
|
||||
val action: String,
|
||||
val resource: String?,
|
||||
val location: String?
|
||||
)
|
||||
|
||||
data class SecurityEvent(
|
||||
val eventId: String,
|
||||
val timestamp: Date,
|
||||
val type: String,
|
||||
val severity: Int
|
||||
)
|
||||
|
||||
enum class ThreatAssessment {
|
||||
NORMAL,
|
||||
SUSPICIOUS,
|
||||
HIGH_RISK,
|
||||
CRITICAL
|
||||
}
|
||||
|
||||
data class ThreatReport(
|
||||
val threats: List<String>,
|
||||
val overallLevel: ThreatLevel
|
||||
)
|
||||
|
||||
enum class ThreatLevel {
|
||||
LOW,
|
||||
MEDIUM,
|
||||
HIGH,
|
||||
CRITICAL
|
||||
}
|
||||
|
||||
138
core/security/src/main/java/com/smoa/core/security/VPNManager.kt
Normal file
138
core/security/src/main/java/com/smoa/core/security/VPNManager.kt
Normal file
@@ -0,0 +1,138 @@
|
||||
package com.smoa.core.security
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* VPN Manager for monitoring and enforcing VPN connections.
|
||||
* Required for browser module and other sensitive operations.
|
||||
*/
|
||||
@Singleton
|
||||
class VPNManager @Inject constructor(
|
||||
private val context: Context
|
||||
) {
|
||||
private val connectivityManager: ConnectivityManager by lazy {
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
}
|
||||
|
||||
private val _vpnState = MutableStateFlow<VPNState>(VPNState.Unknown)
|
||||
val vpnState: StateFlow<VPNState> = _vpnState.asStateFlow()
|
||||
|
||||
/**
|
||||
* Check if VPN is currently connected.
|
||||
*/
|
||||
fun isVPNConnected(): Boolean {
|
||||
return try {
|
||||
val activeNetwork = connectivityManager.activeNetwork
|
||||
val capabilities = activeNetwork?.let {
|
||||
connectivityManager.getNetworkCapabilities(it)
|
||||
}
|
||||
|
||||
capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if VPN is required for the current operation.
|
||||
*/
|
||||
fun isVPNRequired(): Boolean {
|
||||
// VPN is required for browser module and other sensitive operations
|
||||
// This can be made configurable via policy
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Request VPN permission from user.
|
||||
* Returns true if permission is granted or already available.
|
||||
*/
|
||||
suspend fun requestVPNPermission(activity: android.app.Activity): Boolean {
|
||||
return try {
|
||||
val intent = VpnService.prepare(context)
|
||||
if (intent != null) {
|
||||
// VPN permission not granted - need to request
|
||||
_vpnState.value = VPNState.PermissionRequired
|
||||
false
|
||||
} else {
|
||||
// VPN permission already granted
|
||||
_vpnState.value = VPNState.PermissionGranted
|
||||
true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_vpnState.value = VPNState.Error
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor VPN connection state.
|
||||
*/
|
||||
fun startVPNMonitoring() {
|
||||
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
updateVPNState()
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
updateVPNState()
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
updateVPNState()
|
||||
}
|
||||
}
|
||||
|
||||
connectivityManager.registerDefaultNetworkCallback(callback)
|
||||
updateVPNState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update VPN state based on current connection.
|
||||
*/
|
||||
private fun updateVPNState() {
|
||||
_vpnState.value = when {
|
||||
isVPNConnected() -> VPNState.Connected
|
||||
else -> VPNState.Disconnected
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce VPN requirement - throws exception if VPN not connected.
|
||||
*/
|
||||
fun enforceVPNRequirement() {
|
||||
if (isVPNRequired() && !isVPNConnected()) {
|
||||
throw VPNRequiredException("VPN connection required for this operation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* VPN connection states.
|
||||
*/
|
||||
enum class VPNState {
|
||||
Unknown,
|
||||
Connected,
|
||||
Disconnected,
|
||||
PermissionRequired,
|
||||
PermissionGranted,
|
||||
Error
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown when VPN is required but not connected.
|
||||
*/
|
||||
class VPNRequiredException(message: String) : SecurityException(message)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.smoa.core.security
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Zero-trust architecture framework.
|
||||
* Implements "never trust, always verify" principle.
|
||||
*/
|
||||
@Singleton
|
||||
class ZeroTrustFramework @Inject constructor(
|
||||
private val auditLogger: AuditLogger
|
||||
) {
|
||||
|
||||
/**
|
||||
* Verify trust for resource access request.
|
||||
*/
|
||||
suspend fun verifyTrust(
|
||||
userId: String,
|
||||
resource: String,
|
||||
action: String
|
||||
): Result<TrustVerification> {
|
||||
// Zero-trust: verify every access attempt
|
||||
// TODO: Implement comprehensive trust verification
|
||||
return Result.success(TrustVerification(trusted = true, verificationLevel = VerificationLevel.MULTI_FACTOR))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if continuous verification is required.
|
||||
*/
|
||||
suspend fun requiresContinuousVerification(userId: String, sessionId: String): Boolean {
|
||||
// Zero-trust: continuous verification for sensitive operations
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
data class TrustVerification(
|
||||
val trusted: Boolean,
|
||||
val verificationLevel: VerificationLevel,
|
||||
val reason: String? = null
|
||||
)
|
||||
|
||||
enum class VerificationLevel {
|
||||
SINGLE_FACTOR,
|
||||
MULTI_FACTOR,
|
||||
MULTI_FACTOR_BIOMETRIC,
|
||||
HARDWARE_BACKED
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.smoa.core.security.di
|
||||
|
||||
import android.content.Context
|
||||
import com.smoa.core.security.EncryptedDatabaseHelper
|
||||
import com.smoa.core.security.EncryptionManager
|
||||
import com.smoa.core.security.KeyManager
|
||||
import com.smoa.core.security.ScreenProtection
|
||||
import com.smoa.core.security.VPNManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object SecurityModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEncryptionManager(
|
||||
@ApplicationContext context: Context
|
||||
): EncryptionManager {
|
||||
return EncryptionManager(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideKeyManager(
|
||||
@ApplicationContext context: Context,
|
||||
encryptionManager: EncryptionManager
|
||||
): KeyManager {
|
||||
return KeyManager(context, encryptionManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEncryptedDatabaseHelper(
|
||||
encryptionManager: EncryptionManager,
|
||||
keyManager: KeyManager
|
||||
): EncryptedDatabaseHelper {
|
||||
return EncryptedDatabaseHelper(encryptionManager, keyManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideScreenProtection(
|
||||
@ApplicationContext context: Context
|
||||
): ScreenProtection {
|
||||
return ScreenProtection(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVPNManager(
|
||||
@ApplicationContext context: Context
|
||||
): VPNManager {
|
||||
return VPNManager(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.smoa.core.security
|
||||
|
||||
import android.content.Context
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Unit tests for EncryptionManager.
|
||||
*/
|
||||
class EncryptionManagerTest {
|
||||
private val context = mockk<Context>(relaxed = true)
|
||||
private val encryptionManager = EncryptionManager(context)
|
||||
|
||||
@Test
|
||||
fun `getOrCreateEncryptionKey should create key if not exists`() {
|
||||
// Given
|
||||
val alias = "test_key"
|
||||
|
||||
// When
|
||||
val key = encryptionManager.getOrCreateEncryptionKey(alias)
|
||||
|
||||
// Then
|
||||
assertNotNull(key)
|
||||
assertEquals("AES", key.algorithm)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getOrCreateEncryptionKey should return same key for same alias`() {
|
||||
// Given
|
||||
val alias = "test_key"
|
||||
|
||||
// When
|
||||
val key1 = encryptionManager.getOrCreateEncryptionKey(alias)
|
||||
val key2 = encryptionManager.getOrCreateEncryptionKey(alias)
|
||||
|
||||
// Then
|
||||
assertNotNull(key1)
|
||||
assertNotNull(key2)
|
||||
// Keys should be the same for the same alias
|
||||
assertArrayEquals(key1.encoded, key2.encoded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createEncryptedFile should create encrypted file`() {
|
||||
// Given
|
||||
val fileName = "test_file.txt"
|
||||
|
||||
// When
|
||||
val encryptedFile = encryptionManager.createEncryptedFile(fileName)
|
||||
|
||||
// Then
|
||||
assertNotNull(encryptedFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.smoa.core.security
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Unit tests for VPNManager.
|
||||
*/
|
||||
class VPNManagerTest {
|
||||
private val context = mockk<Context>(relaxed = true)
|
||||
private val connectivityManager = mockk<ConnectivityManager>(relaxed = true)
|
||||
|
||||
init {
|
||||
every { context.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isVPNConnected should return true when VPN transport is active`() {
|
||||
// Given
|
||||
val vpnManager = VPNManager(context)
|
||||
val capabilities = mockk<NetworkCapabilities>(relaxed = true)
|
||||
val network = mockk<android.net.Network>(relaxed = true)
|
||||
|
||||
every { connectivityManager.activeNetwork } returns network
|
||||
every { connectivityManager.getNetworkCapabilities(network) } returns capabilities
|
||||
every { capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) } returns true
|
||||
|
||||
// When
|
||||
val result = vpnManager.isVPNConnected()
|
||||
|
||||
// Then
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isVPNConnected should return false when VPN transport is not active`() {
|
||||
// Given
|
||||
val vpnManager = VPNManager(context)
|
||||
val capabilities = mockk<NetworkCapabilities>(relaxed = true)
|
||||
val network = mockk<android.net.Network>(relaxed = true)
|
||||
|
||||
every { connectivityManager.activeNetwork } returns network
|
||||
every { connectivityManager.getNetworkCapabilities(network) } returns capabilities
|
||||
every { capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) } returns false
|
||||
|
||||
// When
|
||||
val result = vpnManager.isVPNConnected()
|
||||
|
||||
// Then
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isVPNRequired should return true by default`() {
|
||||
// Given
|
||||
val vpnManager = VPNManager(context)
|
||||
|
||||
// When
|
||||
val result = vpnManager.isVPNRequired()
|
||||
|
||||
// Then
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `enforceVPNRequirement should throw exception when VPN not connected`() {
|
||||
// Given
|
||||
val vpnManager = VPNManager(context)
|
||||
val capabilities = mockk<NetworkCapabilities>(relaxed = true)
|
||||
val network = mockk<android.net.Network>(relaxed = true)
|
||||
|
||||
every { connectivityManager.activeNetwork } returns network
|
||||
every { connectivityManager.getNetworkCapabilities(network) } returns capabilities
|
||||
every { capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) } returns false
|
||||
|
||||
// When/Then
|
||||
try {
|
||||
vpnManager.enforceVPNRequirement()
|
||||
fail("Should have thrown VPNRequiredException")
|
||||
} catch (e: VPNRequiredException) {
|
||||
assertTrue(e.message?.contains("VPN connection required") == true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user