Monorepo: Gitea CI, docs, auth/sync, backend APIs, gitignore

- Add Gitea Actions workflow; point README to gitea.d-bis.org/Sankofa_Phoenix/SMOA
- Expand .gitignore for Spring H2 data, secrets, Kotlin .kotlin/, tooling
- Track docs/api/generated ReDoc bundle; refresh api docs README
- Android: network/auth/sync, UI shell, tests; backend credentials/integrity APIs
- Docs, scripts (generate-api-docs), modules and core updates

Made-with: Cursor
This commit is contained in:
defiQUG
2026-03-23 20:19:24 -07:00
parent f97e23592c
commit a2dc194a49
234 changed files with 10008 additions and 1149 deletions
+13 -1
View File
@@ -1,8 +1,8 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
@@ -11,6 +11,7 @@ android {
defaultConfig {
minSdk = AppConfig.minSdk
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
@@ -29,6 +30,14 @@ android {
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
}
hilt {
enableAggregatingTask = true
}
}
kapt {
correctErrorTypes = true
}
dependencies {
@@ -59,4 +68,7 @@ dependencies {
testImplementation(Dependencies.mockk)
testImplementation(Dependencies.coroutinesTest)
testImplementation(Dependencies.truth)
androidTestImplementation(Dependencies.androidxJunit)
androidTestImplementation("androidx.test:core:1.5.0")
}
@@ -0,0 +1,24 @@
package com.smoa.core.security
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class EncryptionManagerInstrumentedTest {
@Test
fun getOrCreateEncryptionKey_usesAndroidKeyStore() {
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
val manager = EncryptionManager(context)
val alias = "instrumented_smoa_enc_key"
val key = manager.getOrCreateEncryptionKey(alias)
assertNotNull(key)
assertEquals("AES", key.algorithm)
}
}
@@ -4,7 +4,7 @@ import android.content.Context
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import net.zetetic.database.sqlcipher.SupportOpenHelperFactory
import javax.crypto.SecretKey
import java.security.SecureRandom
import javax.inject.Inject
import javax.inject.Singleton
@@ -14,11 +14,12 @@ import javax.inject.Singleton
*/
@Singleton
class EncryptedDatabaseHelper @Inject constructor(
private val encryptionManager: EncryptionManager,
private val keyManager: KeyManager
) {
companion object {
private const val KEY_ALIAS_PREFIX = "db_encryption_key_"
/** Raw key length for SQLCipher (256-bit AES). */
private const val RAW_KEY_BYTES = 32
}
/**
@@ -33,14 +34,11 @@ class EncryptedDatabaseHelper @Inject constructor(
// 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)
// AndroidKeyStore SecretKey.getEncoded() is null for hardware-backed keys — unusable for SQLCipher.
// Generate random raw key material and persist via EncryptedSharedPreferences.
val keyBytes = ByteArray(RAW_KEY_BYTES).also { SecureRandom().nextBytes(it) }
val encodedKey = android.util.Base64.encodeToString(keyBytes, android.util.Base64.DEFAULT)
keyManager.putSecureString("$KEY_ALIAS_PREFIX$alias", encodedKey)
keyBytes
}
}
@@ -3,11 +3,9 @@ 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
@@ -32,11 +30,8 @@ class ScreenProtection @Inject constructor(
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)
}
// Do not call setDecorFitsSystemWindows(false) here: it breaks Scaffold/WindowInsets on
// foldables unless every screen consumes insets; FLAG_SECURE alone blocks screenshots.
}
/**
@@ -52,6 +52,21 @@ class VPNManager @Inject constructor(
return true
}
@Volatile
private var browserVpnEnforcedFlag: Boolean = false
/**
* Set from app [com.smoa.BuildConfig.SMOA_BROWSER_VPN_ENFORCED] or policy at startup.
*/
fun setBrowserVpnEnforced(enforced: Boolean) {
browserVpnEnforcedFlag = enforced
}
/**
* When true, in-app browser navigation enforces an active VPN first.
*/
fun isBrowserVPNEnforced(): Boolean = browserVpnEnforcedFlag
/**
* Request VPN permission from user.
* Returns true if permission is granted or already available.
@@ -1,7 +1,6 @@
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
@@ -33,15 +32,6 @@ object SecurityModule {
return KeyManager(context, encryptionManager)
}
@Provides
@Singleton
fun provideEncryptedDatabaseHelper(
encryptionManager: EncryptionManager,
keyManager: KeyManager
): EncryptedDatabaseHelper {
return EncryptedDatabaseHelper(encryptionManager, keyManager)
}
@Provides
@Singleton
fun provideScreenProtection(
@@ -2,55 +2,49 @@ package com.smoa.core.security
import android.content.Context
import io.mockk.mockk
import org.junit.Assert.*
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Ignore
import org.junit.Test
/**
* Unit tests for EncryptionManager.
* `EncryptionManager` opens **AndroidKeyStore** in its constructor; that provider is not available on the
* host JVM for unit tests. Prefer instrumented tests on a device/emulator for this class.
*/
@Ignore("AndroidKeyStore not available in JVM unit tests")
class EncryptionManagerTest {
private val context = mockk<Context>(relaxed = true)
private val encryptionManager = EncryptionManager(context)
private val encryptionManager get() = EncryptionManager(context)
@Test
fun `getOrCreateEncryptionKey should create key if not exists`() {
// Given
val alias = "test_key"
val alias = "test_key_encryption_manager"
// 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"
val alias = "test_key_encryption_manager_same"
// 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"
val fileName = "test_file_encryption_manager.txt"
// When
val encryptedFile = encryptionManager.createEncryptedFile(fileName)
// Then
assertNotNull(encryptedFile)
}
}
@@ -67,6 +67,16 @@ class VPNManagerTest {
assertTrue(result)
}
@Test
fun `isBrowserVPNEnforced follows setBrowserVpnEnforced`() {
val vpnManager = VPNManager(context)
assertFalse(vpnManager.isBrowserVPNEnforced())
vpnManager.setBrowserVpnEnforced(true)
assertTrue(vpnManager.isBrowserVPNEnforced())
vpnManager.setBrowserVpnEnforced(false)
assertFalse(vpnManager.isBrowserVPNEnforced())
}
@Test
fun `enforceVPNRequirement should throw exception when VPN not connected`() {
// Given