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,45 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.smoa.core.auth"
compileSdk = AppConfig.compileSdk
defaultConfig {
minSdk = AppConfig.minSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation(project(":core:common"))
implementation(project(":core:security"))
implementation(Dependencies.androidxCoreKtx)
implementation(Dependencies.androidxLifecycleRuntimeKtx)
implementation(Dependencies.securityCrypto)
implementation(Dependencies.biometric)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.coroutinesAndroid)
implementation(Dependencies.hiltAndroid)
kapt(Dependencies.hiltAndroidCompiler)
// Testing
testImplementation(Dependencies.junit)
testImplementation(Dependencies.mockk)
testImplementation(Dependencies.coroutinesTest)
testImplementation(Dependencies.truth)
}

View File

@@ -0,0 +1,134 @@
package com.smoa.core.auth
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AuthCoordinator @Inject constructor(
private val pinManager: PinManager,
private val biometricManager: BiometricManager,
private val dualBiometricManager: DualBiometricManager,
private val sessionManager: SessionManager
) {
private val _authState = MutableStateFlow<AuthState>(AuthState.Unauthenticated)
val authState: StateFlow<AuthState> = _authState.asStateFlow()
/**
* Initiate three-factor authentication flow.
* Requires: PIN + Fingerprint + Facial Recognition
*/
suspend fun authenticate(
pin: String,
activity: FragmentActivity,
onBiometricSuccess: () -> Unit,
onError: (String) -> Unit
): AuthResult {
// Factor 1: Verify PIN
val pinResult = pinManager.verifyPin(pin)
when (pinResult) {
is PinManager.PinVerificationResult.Success -> {
// PIN verified, proceed to biometrics
}
is PinManager.PinVerificationResult.Failed -> {
return AuthResult.Failure(
"PIN incorrect. ${pinResult.remainingAttempts} attempts remaining."
)
}
PinManager.PinVerificationResult.Locked -> {
return AuthResult.Failure("Account locked due to too many failed attempts.")
}
PinManager.PinVerificationResult.NotSet -> {
return AuthResult.Failure("PIN not set. Please set up authentication.")
}
}
// Factor 2 & 3: Dual biometric authentication (fingerprint + facial recognition)
// Both must pass sequentially for true three-factor authentication
if (!dualBiometricManager.areBothBiometricsAvailable()) {
return AuthResult.Failure("Biometric authentication not available. Please enroll fingerprint and face.")
}
// Perform dual biometric authentication (fingerprint then face)
val dualBiometricResult = dualBiometricManager.authenticateDualBiometric(
activity = activity,
onProgress = { message ->
// Progress updates can be shown to user
}
)
return when (dualBiometricResult) {
is DualBiometricResult.Success -> {
// All three factors verified - create session
sessionManager.startSession()
_authState.value = AuthState.Authenticated
onBiometricSuccess()
AuthResult.Success
}
is DualBiometricResult.Failure -> {
AuthResult.Failure("Biometric authentication failed: ${dualBiometricResult.message}")
}
is DualBiometricResult.Cancelled -> {
AuthResult.Cancelled
}
is DualBiometricResult.NotAvailable -> {
AuthResult.Failure("Biometric authentication not available")
}
}
}
/**
* Step-up authentication for sensitive operations.
*/
suspend fun stepUpAuthentication(
pin: String,
activity: FragmentActivity,
onSuccess: () -> Unit,
onError: (String) -> Unit
): AuthResult {
// For step-up, we require PIN + biometric again
return authenticate(pin, activity, onSuccess, onError)
}
/**
* Check if user is currently authenticated.
*/
fun isAuthenticated(): Boolean {
return sessionManager.isSessionActive() && _authState.value is AuthState.Authenticated
}
/**
* Lock the session (manual lock).
*/
fun lock() {
sessionManager.endSession()
_authState.value = AuthState.Locked
}
/**
* Logout and clear session.
*/
fun logout() {
sessionManager.endSession()
_authState.value = AuthState.Unauthenticated
}
sealed class AuthState {
object Unauthenticated : AuthState()
object Authenticated : AuthState()
object Locked : AuthState()
data class Authenticating(val factorsCompleted: Int) : AuthState()
}
sealed class AuthResult {
object Success : AuthResult()
data class Failure(val message: String) : AuthResult()
object Cancelled : AuthResult()
object Pending : AuthResult()
}
}

View File

@@ -0,0 +1,109 @@
package com.smoa.core.auth
import android.content.Context
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BiometricManager @Inject constructor(
@ApplicationContext private val context: Context
) {
/**
* Check if biometric authentication is available on the device.
*/
fun isBiometricAvailable(): BiometricAvailability {
val biometricManager = BiometricManager.from(context)
return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
BiometricManager.BIOMETRIC_SUCCESS -> BiometricAvailability.Available
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> BiometricAvailability.NoHardware
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> BiometricAvailability.HardwareUnavailable
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> BiometricAvailability.NotEnrolled
else -> BiometricAvailability.Unknown
}
}
/**
* Check if fingerprint authentication is available.
*/
fun isFingerprintAvailable(): Boolean {
val biometricManager = BiometricManager.from(context)
return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) ==
BiometricManager.BIOMETRIC_SUCCESS
}
/**
* Check if facial recognition is available.
*/
fun isFacialRecognitionAvailable(): Boolean {
val biometricManager = BiometricManager.from(context)
return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) ==
BiometricManager.BIOMETRIC_SUCCESS
}
/**
* Create a biometric prompt for authentication.
* Requires both fingerprint and facial recognition (both factors must pass).
*/
fun createBiometricPrompt(
activity: FragmentActivity,
onSuccess: () -> Unit,
onError: (String) -> Unit,
onCancel: () -> Unit
): BiometricPrompt {
val executor = ContextCompat.getMainExecutor(context)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
onSuccess()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
when (errorCode) {
BiometricPrompt.ERROR_USER_CANCELED,
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> onCancel()
else -> onError(errString.toString())
}
}
override fun onAuthenticationFailed() {
onError("Biometric authentication failed")
}
}
return BiometricPrompt(activity, executor, callback)
}
/**
* Prompt for biometric authentication (fingerprint or face).
*/
fun authenticate(
activity: FragmentActivity,
onSuccess: () -> Unit,
onError: (String) -> Unit,
onCancel: () -> Unit
) {
val prompt = createBiometricPrompt(activity, onSuccess, onError, onCancel)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric Authentication")
.setSubtitle("Use your fingerprint or face to authenticate")
.setNegativeButtonText("Cancel")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build()
prompt.authenticate(promptInfo)
}
enum class BiometricAvailability {
Available,
NoHardware,
HardwareUnavailable,
NotEnrolled,
Unknown
}
}

View File

@@ -0,0 +1,171 @@
package com.smoa.core.auth
import android.content.Context
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.suspendCancellableCoroutine
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
/**
* Dual Biometric Manager for true three-factor authentication.
* Requires: PIN + Fingerprint + Facial Recognition (both biometrics must pass).
*
* Note: Android's BiometricPrompt API doesn't support requiring both
* fingerprint AND face separately in a single prompt. This implementation
* uses sequential prompts to require both factors.
*/
@Singleton
class DualBiometricManager @Inject constructor(
@ApplicationContext private val context: Context,
private val biometricManager: BiometricManager
) {
/**
* Authenticate with both fingerprint and facial recognition sequentially.
* Both must succeed for authentication to pass.
*/
suspend fun authenticateDualBiometric(
activity: FragmentActivity,
onProgress: (String) -> Unit = {}
): DualBiometricResult {
// Step 1: Fingerprint authentication
onProgress("Please authenticate with your fingerprint")
val fingerprintResult = authenticateFingerprint(activity)
when (fingerprintResult) {
is DualBiometricResult.Success -> {
// Fingerprint passed, proceed to face
}
is DualBiometricResult.Failure -> {
return DualBiometricResult.Failure("Fingerprint authentication failed: ${fingerprintResult.message}")
}
is DualBiometricResult.Cancelled -> {
return DualBiometricResult.Cancelled
}
else -> return fingerprintResult
}
// Step 2: Facial recognition authentication
onProgress("Please authenticate with your face")
val faceResult = authenticateFacialRecognition(activity)
return when (faceResult) {
is DualBiometricResult.Success -> {
DualBiometricResult.Success("Both biometric factors verified")
}
is DualBiometricResult.Failure -> {
DualBiometricResult.Failure("Facial recognition failed: ${faceResult.message}")
}
is DualBiometricResult.Cancelled -> {
DualBiometricResult.Cancelled
}
else -> faceResult
}
}
/**
* Authenticate with fingerprint only.
*/
private suspend fun authenticateFingerprint(
activity: FragmentActivity
): DualBiometricResult = suspendCancellableCoroutine { continuation ->
val executor = ContextCompat.getMainExecutor(context)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
continuation.resume(DualBiometricResult.Success("Fingerprint verified"))
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
when (errorCode) {
BiometricPrompt.ERROR_USER_CANCELED,
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
continuation.resume(DualBiometricResult.Cancelled)
}
else -> {
continuation.resume(DualBiometricResult.Failure(errString.toString()))
}
}
}
override fun onAuthenticationFailed() {
continuation.resume(DualBiometricResult.Failure("Fingerprint authentication failed"))
}
}
val prompt = BiometricPrompt(activity, executor, callback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Fingerprint Authentication")
.setSubtitle("Use your fingerprint to continue")
.setNegativeButtonText("Cancel")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build()
prompt.authenticate(promptInfo)
}
/**
* Authenticate with facial recognition only.
*/
private suspend fun authenticateFacialRecognition(
activity: FragmentActivity
): DualBiometricResult = suspendCancellableCoroutine { continuation ->
val executor = ContextCompat.getMainExecutor(context)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
continuation.resume(DualBiometricResult.Success("Face verified"))
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
when (errorCode) {
BiometricPrompt.ERROR_USER_CANCELED,
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
continuation.resume(DualBiometricResult.Cancelled)
}
else -> {
continuation.resume(DualBiometricResult.Failure(errString.toString()))
}
}
}
override fun onAuthenticationFailed() {
continuation.resume(DualBiometricResult.Failure("Facial recognition failed"))
}
}
val prompt = BiometricPrompt(activity, executor, callback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Facial Recognition")
.setSubtitle("Use your face to continue")
.setNegativeButtonText("Cancel")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build()
prompt.authenticate(promptInfo)
}
/**
* Check if both fingerprint and facial recognition are available.
*/
fun areBothBiometricsAvailable(): Boolean {
val biometricManager = BiometricManager.from(context)
return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) ==
BiometricManager.BIOMETRIC_SUCCESS
}
}
/**
* Result of dual biometric authentication.
*/
sealed class DualBiometricResult {
data class Success(val message: String) : DualBiometricResult()
data class Failure(val message: String) : DualBiometricResult()
object Cancelled : DualBiometricResult()
object NotAvailable : DualBiometricResult()
}

View File

@@ -0,0 +1,148 @@
package com.smoa.core.auth
import com.smoa.core.common.Result
import com.smoa.core.security.KeyManager
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PinManager @Inject constructor(
private val keyManager: KeyManager
) {
companion object {
private const val PIN_KEY = "user_pin_hash"
private const val PIN_RETRY_COUNT_KEY = "pin_retry_count"
private const val MAX_RETRY_ATTEMPTS = 5
private const val MIN_PIN_LENGTH = 6
private const val MAX_PIN_LENGTH = 12
}
/**
* Set a new PIN after validating complexity requirements.
*/
fun setPin(pin: String): Result<Unit> {
return try {
validatePinComplexity(pin)
val pinHash = hashPin(pin)
keyManager.putSecureString(PIN_KEY, pinHash)
keyManager.putSecureString(PIN_RETRY_COUNT_KEY, "0")
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Verify a PIN against the stored hash.
*/
fun verifyPin(pin: String): PinVerificationResult {
val currentRetries = getRetryCount()
if (currentRetries >= MAX_RETRY_ATTEMPTS) {
return PinVerificationResult.Locked
}
val storedHash = keyManager.getSecureString(PIN_KEY)
if (storedHash == null) {
return PinVerificationResult.NotSet
}
val inputHash = hashPin(pin)
val isValid = storedHash == inputHash
if (isValid) {
keyManager.putSecureString(PIN_RETRY_COUNT_KEY, "0")
return PinVerificationResult.Success
} else {
val newRetryCount = currentRetries + 1
keyManager.putSecureString(PIN_RETRY_COUNT_KEY, newRetryCount.toString())
return if (newRetryCount >= MAX_RETRY_ATTEMPTS) {
PinVerificationResult.Locked
} else {
PinVerificationResult.Failed(remainingAttempts = MAX_RETRY_ATTEMPTS - newRetryCount)
}
}
}
/**
* Check if a PIN is set.
*/
fun isPinSet(): Boolean {
return keyManager.getSecureString(PIN_KEY) != null
}
/**
* Get the current retry count.
*/
fun getRetryCount(): Int {
return keyManager.getSecureString(PIN_RETRY_COUNT_KEY)?.toIntOrNull() ?: 0
}
/**
* Reset retry count (used after successful authentication).
*/
fun resetRetryCount() {
keyManager.putSecureString(PIN_RETRY_COUNT_KEY, "0")
}
/**
* Check if account is locked due to too many failed attempts.
*/
fun isLocked(): Boolean {
return getRetryCount() >= MAX_RETRY_ATTEMPTS
}
/**
* Validate PIN complexity requirements.
*/
private fun validatePinComplexity(pin: String) {
if (pin.length < MIN_PIN_LENGTH || pin.length > MAX_PIN_LENGTH) {
throw IllegalArgumentException("PIN must be between $MIN_PIN_LENGTH and $MAX_PIN_LENGTH characters")
}
if (!pin.all { it.isDigit() }) {
throw IllegalArgumentException("PIN must contain only digits")
}
// Check for simple patterns (e.g., 111111, 123456)
if (pin.all { it == pin[0] } || isSequential(pin)) {
throw IllegalArgumentException("PIN cannot be a simple pattern")
}
}
/**
* Hash PIN using SHA-256 (in production, use a proper password hashing algorithm like bcrypt).
*/
private fun hashPin(pin: String): String {
val digest = java.security.MessageDigest.getInstance("SHA-256")
val hashBytes = digest.digest(pin.toByteArray())
return hashBytes.joinToString("") { "%02x".format(it) }
}
/**
* Check if PIN is sequential (e.g., 123456, 654321).
*/
private fun isSequential(pin: String): Boolean {
var isAscending = true
var isDescending = true
for (i in 1 until pin.length) {
val current = pin[i].digitToInt()
val previous = pin[i - 1].digitToInt()
if (current != previous + 1) isAscending = false
if (current != previous - 1) isDescending = false
}
return isAscending || isDescending
}
sealed class PinVerificationResult {
object Success : PinVerificationResult()
data class Failed(val remainingAttempts: Int) : PinVerificationResult()
object Locked : PinVerificationResult()
object NotSet : PinVerificationResult()
}
}

View File

@@ -0,0 +1,86 @@
package com.smoa.core.auth
import com.smoa.core.security.KeyManager
import javax.inject.Inject
import javax.inject.Singleton
/**
* Policy Manager for dynamic policy enforcement and updates.
*/
@Singleton
class PolicyManager @Inject constructor(
private val keyManager: KeyManager
) {
companion object {
private const val POLICY_VERSION_KEY = "policy_version"
private const val POLICY_DATA_KEY = "policy_data"
private const val SESSION_TIMEOUT_KEY = "session_timeout_ms"
private const val OFFLINE_TIMEOUT_KEY = "offline_timeout_ms"
private const val LOCK_ON_FOLD_KEY = "lock_on_fold"
private const val LOCK_ON_BACKGROUND_KEY = "lock_on_background"
}
/**
* Policy data structure.
*/
data class Policy(
val version: Int,
val sessionTimeoutMs: Long,
val offlineTimeoutMs: Long,
val lockOnFold: Boolean,
val lockOnBackground: Boolean,
val allowedModules: Set<String>,
val allowedUrls: Set<String>
)
/**
* Get current policy version.
*/
fun getPolicyVersion(): Int {
return keyManager.getSecureString(POLICY_VERSION_KEY)?.toIntOrNull() ?: 0
}
/**
* Update policy from server (should be called on trusted connectivity).
*/
fun updatePolicy(policy: Policy) {
keyManager.putSecureString(POLICY_VERSION_KEY, policy.version.toString())
keyManager.putSecureString(SESSION_TIMEOUT_KEY, policy.sessionTimeoutMs.toString())
keyManager.putSecureString(OFFLINE_TIMEOUT_KEY, policy.offlineTimeoutMs.toString())
keyManager.putSecureString(LOCK_ON_FOLD_KEY, policy.lockOnFold.toString())
keyManager.putSecureString(LOCK_ON_BACKGROUND_KEY, policy.lockOnBackground.toString())
// Store policy data as JSON (simplified - use proper serialization in production)
keyManager.putSecureString(POLICY_DATA_KEY, policy.toString())
}
/**
* Get session timeout from policy.
*/
fun getSessionTimeoutMs(): Long {
return keyManager.getSecureString(SESSION_TIMEOUT_KEY)?.toLongOrNull()
?: 30 * 60 * 1000L // Default 30 minutes
}
/**
* Get offline timeout from policy.
*/
fun getOfflineTimeoutMs(): Long {
return keyManager.getSecureString(OFFLINE_TIMEOUT_KEY)?.toLongOrNull()
?: 7 * 24 * 60 * 60 * 1000L // Default 7 days
}
/**
* Check if lock on fold is enabled.
*/
fun shouldLockOnFold(): Boolean {
return keyManager.getSecureString(LOCK_ON_FOLD_KEY)?.toBoolean() ?: false
}
/**
* Check if lock on background is enabled.
*/
fun shouldLockOnBackground(): Boolean {
return keyManager.getSecureString(LOCK_ON_BACKGROUND_KEY)?.toBoolean() ?: true
}
}

View File

@@ -0,0 +1,150 @@
package com.smoa.core.auth
import javax.inject.Inject
import javax.inject.Singleton
/**
* Role-Based Access Control framework for SMOA.
* Enforces access control at module, feature, and data levels.
*/
@Singleton
class RBACFramework @Inject constructor() {
/**
* User role definitions.
*/
enum class Role {
ADMIN,
OPERATOR,
VIEWER,
GUEST
}
/**
* Permission definitions for modules and features.
*/
enum class Permission {
// Credentials module
VIEW_CREDENTIALS,
DISPLAY_CREDENTIALS,
// Directory module
VIEW_DIRECTORY,
SEARCH_DIRECTORY,
VIEW_UNIT_DIRECTORY,
// Communications module
USE_RADIO,
JOIN_CHANNEL,
CREATE_CHANNEL,
// Meetings module
JOIN_MEETING,
HOST_MEETING,
SCREEN_SHARE,
// Browser module
ACCESS_BROWSER,
NAVIGATE_URL
}
/**
* Module access definitions.
*/
enum class Module {
CREDENTIALS,
DIRECTORY,
COMMUNICATIONS,
MEETINGS,
BROWSER
}
/**
* Check if a role has a specific permission.
*/
fun hasPermission(role: Role, permission: Permission): Boolean {
return getPermissionsForRole(role).contains(permission)
}
/**
* Check if a role can access a module.
*/
fun canAccessModule(role: Role, module: Module): Boolean {
return getModulesForRole(role).contains(module)
}
/**
* Get all permissions for a role.
*/
private fun getPermissionsForRole(role: Role): Set<Permission> {
return when (role) {
Role.ADMIN -> setOf(
Permission.VIEW_CREDENTIALS,
Permission.DISPLAY_CREDENTIALS,
Permission.VIEW_DIRECTORY,
Permission.SEARCH_DIRECTORY,
Permission.VIEW_UNIT_DIRECTORY,
Permission.USE_RADIO,
Permission.JOIN_CHANNEL,
Permission.CREATE_CHANNEL,
Permission.JOIN_MEETING,
Permission.HOST_MEETING,
Permission.SCREEN_SHARE,
Permission.ACCESS_BROWSER,
Permission.NAVIGATE_URL
)
Role.OPERATOR -> setOf(
Permission.VIEW_CREDENTIALS,
Permission.DISPLAY_CREDENTIALS,
Permission.VIEW_DIRECTORY,
Permission.SEARCH_DIRECTORY,
Permission.VIEW_UNIT_DIRECTORY,
Permission.USE_RADIO,
Permission.JOIN_CHANNEL,
Permission.JOIN_MEETING,
Permission.SCREEN_SHARE,
Permission.ACCESS_BROWSER
)
Role.VIEWER -> setOf(
Permission.VIEW_CREDENTIALS,
Permission.VIEW_DIRECTORY,
Permission.SEARCH_DIRECTORY,
Permission.JOIN_MEETING
)
Role.GUEST -> setOf(
Permission.VIEW_CREDENTIALS
)
}
}
/**
* Get all modules accessible by a role.
*/
private fun getModulesForRole(role: Role): Set<Module> {
return when (role) {
Role.ADMIN -> setOf(
Module.CREDENTIALS,
Module.DIRECTORY,
Module.COMMUNICATIONS,
Module.MEETINGS,
Module.BROWSER
)
Role.OPERATOR -> setOf(
Module.CREDENTIALS,
Module.DIRECTORY,
Module.COMMUNICATIONS,
Module.MEETINGS,
Module.BROWSER
)
Role.VIEWER -> setOf(
Module.CREDENTIALS,
Module.DIRECTORY,
Module.MEETINGS
)
Role.GUEST -> setOf(
Module.CREDENTIALS
)
}
}
}

View File

@@ -0,0 +1,112 @@
package com.smoa.core.auth
import com.smoa.core.security.KeyManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SessionManager @Inject constructor(
private val keyManager: KeyManager
) {
companion object {
private const val SESSION_START_TIME_KEY = "session_start_time"
private const val SESSION_ACTIVE_KEY = "session_active"
private const val DEFAULT_SESSION_TIMEOUT_MS = 30 * 60 * 1000L // 30 minutes
}
private val _sessionState = MutableStateFlow<SessionState>(SessionState.Inactive)
val sessionState: StateFlow<SessionState> = _sessionState.asStateFlow()
private var sessionStartTime: Long = 0
var sessionTimeoutMs: Long = DEFAULT_SESSION_TIMEOUT_MS
/**
* Start a new session.
*/
fun startSession() {
sessionStartTime = System.currentTimeMillis()
keyManager.putSecureString(SESSION_START_TIME_KEY, sessionStartTime.toString())
keyManager.putSecureString(SESSION_ACTIVE_KEY, "true")
_sessionState.value = SessionState.Active(sessionStartTime)
}
/**
* End the current session.
*/
fun endSession() {
sessionStartTime = 0
keyManager.putSecureString(SESSION_START_TIME_KEY, "0")
keyManager.putSecureString(SESSION_ACTIVE_KEY, "false")
_sessionState.value = SessionState.Inactive
}
/**
* Check if session is currently active.
*/
fun isSessionActive(): Boolean {
val storedActive = keyManager.getSecureString(SESSION_ACTIVE_KEY) == "true"
val storedStartTime = keyManager.getSecureString(SESSION_START_TIME_KEY)?.toLongOrNull() ?: 0
if (!storedActive || storedStartTime == 0L) {
return false
}
val elapsed = System.currentTimeMillis() - storedStartTime
if (elapsed > sessionTimeoutMs) {
// Session expired
endSession()
return false
}
return true
}
/**
* Check if session has expired.
*/
fun isSessionExpired(): Boolean {
if (sessionStartTime == 0L) return true
val elapsed = System.currentTimeMillis() - sessionStartTime
return elapsed > sessionTimeoutMs
}
/**
* Get remaining session time in milliseconds.
*/
fun getRemainingSessionTime(): Long {
if (sessionStartTime == 0L) return 0
val elapsed = System.currentTimeMillis() - sessionStartTime
return maxOf(0, sessionTimeoutMs - elapsed)
}
/**
* Restore session state from storage.
*/
fun restoreSession(): Boolean {
val storedActive = keyManager.getSecureString(SESSION_ACTIVE_KEY) == "true"
val storedStartTime = keyManager.getSecureString(SESSION_START_TIME_KEY)?.toLongOrNull() ?: 0
if (storedActive && storedStartTime > 0) {
val elapsed = System.currentTimeMillis() - storedStartTime
if (elapsed <= sessionTimeoutMs) {
sessionStartTime = storedStartTime
_sessionState.value = SessionState.Active(storedStartTime)
return true
} else {
// Session expired, clear it
endSession()
}
}
return false
}
sealed class SessionState {
object Inactive : SessionState()
data class Active(val startTime: Long) : SessionState()
object Expired : SessionState()
}
}

View File

@@ -0,0 +1,65 @@
package com.smoa.core.auth
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
import javax.inject.Singleton
/**
* User session manager.
* Tracks current user's role, unit, and session information.
*/
@Singleton
class UserSession @Inject constructor() {
private val _currentUser = MutableStateFlow<UserInfo?>(null)
val currentUser: StateFlow<UserInfo?> = _currentUser.asStateFlow()
/**
* Set current user session.
*/
fun setUser(userInfo: UserInfo) {
_currentUser.value = userInfo
}
/**
* Clear user session.
*/
fun clearUser() {
_currentUser.value = null
}
/**
* Get current user role.
*/
fun getCurrentRole(): RBACFramework.Role {
return _currentUser.value?.role ?: RBACFramework.Role.GUEST
}
/**
* Get current user unit.
*/
fun getCurrentUnit(): String? {
return _currentUser.value?.unit
}
/**
* Get current user ID.
*/
fun getCurrentUserId(): String? {
return _currentUser.value?.userId
}
}
/**
* User information.
*/
data class UserInfo(
val userId: String,
val userName: String,
val role: RBACFramework.Role,
val unit: String?,
val clearanceLevel: String?,
val missionAssignment: String?
)

View File

@@ -0,0 +1,67 @@
package com.smoa.core.auth.di
import android.content.Context
import com.smoa.core.auth.BiometricManager
import com.smoa.core.auth.DualBiometricManager
import com.smoa.core.auth.PinManager
import com.smoa.core.auth.SessionManager
import com.smoa.core.auth.UserSession
import com.smoa.core.auth.RBACFramework
import com.smoa.core.security.KeyManager
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 AuthModule {
@Provides
@Singleton
fun providePinManager(
keyManager: KeyManager
): PinManager {
return PinManager(keyManager)
}
@Provides
@Singleton
fun provideBiometricManager(
@ApplicationContext context: Context
): BiometricManager {
return BiometricManager(context)
}
@Provides
@Singleton
fun provideDualBiometricManager(
@ApplicationContext context: Context
): DualBiometricManager {
// DualBiometricManager uses androidx.biometric.BiometricManager directly
val biometricManager = androidx.biometric.BiometricManager.from(context)
return DualBiometricManager(context, biometricManager)
}
@Provides
@Singleton
fun provideSessionManager(
keyManager: KeyManager
): SessionManager {
return SessionManager(keyManager)
}
@Provides
@Singleton
fun provideUserSession(): UserSession {
return UserSession()
}
@Provides
@Singleton
fun provideRBACFramework(): RBACFramework {
return RBACFramework()
}
}

View File

@@ -0,0 +1,111 @@
package com.smoa.core.auth
import com.smoa.core.common.TestCoroutineRule
import com.smoa.core.security.KeyManager
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test
/**
* Unit tests for PinManager.
*/
class PinManagerTest {
@get:Rule
val testCoroutineRule = TestCoroutineRule()
private val keyManager = mockk<KeyManager>(relaxed = true)
private val pinManager = PinManager(keyManager)
@Test
fun `setPin should store encrypted PIN`() = runTest {
// Given
val pin = "123456"
every { keyManager.putSecureString(any(), any()) } returns Unit
// When
val result = pinManager.setPin(pin)
// Then
assertTrue(result.isSuccess)
verify { keyManager.putSecureString("user_pin", any()) }
}
@Test
fun `setPin should fail for invalid PIN length`() = runTest {
// Given
val shortPin = "12345" // Too short
val longPin = "1234567890123" // Too long
// When
val shortResult = pinManager.setPin(shortPin)
val longResult = pinManager.setPin(longPin)
// Then
assertTrue(shortResult.isFailure)
assertTrue(longResult.isFailure)
}
@Test
fun `verifyPin should return success for correct PIN`() = runTest {
// Given
val pin = "123456"
val hashedPin = "hashed_pin_value"
every { keyManager.getSecureString("user_pin") } returns hashedPin
every { keyManager.putSecureString(any(), any()) } returns Unit
// Set PIN first
pinManager.setPin(pin)
// When
val result = pinManager.verifyPin(pin)
// Then
assertTrue(result is PinManager.PinVerificationResult.Success)
}
@Test
fun `verifyPin should return failed for incorrect PIN`() = runTest {
// Given
val correctPin = "123456"
val wrongPin = "654321"
every { keyManager.getSecureString("user_pin") } returns "hashed_pin"
every { keyManager.putSecureString(any(), any()) } returns Unit
// Set PIN first
pinManager.setPin(correctPin)
// When
val result = pinManager.verifyPin(wrongPin)
// Then
assertTrue(result is PinManager.PinVerificationResult.Failed)
if (result is PinManager.PinVerificationResult.Failed) {
assertTrue(result.remainingAttempts < 5)
}
}
@Test
fun `verifyPin should lock after max attempts`() = runTest {
// Given
val correctPin = "123456"
val wrongPin = "654321"
every { keyManager.getSecureString("user_pin") } returns "hashed_pin"
every { keyManager.putSecureString(any(), any()) } returns Unit
pinManager.setPin(correctPin)
// When - attempt wrong PIN multiple times
repeat(5) {
pinManager.verifyPin(wrongPin)
}
// Then
val result = pinManager.verifyPin(wrongPin)
assertTrue(result is PinManager.PinVerificationResult.Locked)
}
}