Initial commit

This commit is contained in:
defiQUG
2025-12-26 10:48:33 -08:00
commit 97f75e144f
270 changed files with 35886 additions and 0 deletions

View 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)
}

View File

@@ -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
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View 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)

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}