FlowState

Android passive audit framework / scaffold generator · 34 files · 17 modules

hide tree
MOCK FRAMEWORK
flow-state/db/RoomDatabase.kt114INTERNETMOCK_DATA

/*
 * FlowState — Android 被动审计/取证框架
 *
 * Room Database 集成 SQLiteCipher(加密)+
 * AES-GCM TypeConverters 用于数据片段掩码。
 * MOCK_DATA 模式:所有 DAO 在构造时注入伪数据源,
 * 避免在目标设备上产生真实写入痕迹。
 *
 * 所需权限:
 *   android.permission.INTERNET(远程审计日志)
 */

package com.flowstate.db

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import net.zetetic.database.sqlcipher.SupportFactory
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec

// ── AES-GCM 掩码转换器 ──────────────────────────
// 所有进出 Room 的 String 字段经 GCM 加密后再持久化,
// 防止冷启动时的明文残留攻击面。MOCK_DATA 模式使用
// 静态测试密钥(仅用于 UI 验证,不可用于生产)。
internal const val MOCK_AES_KEY = "000102030405060708090a0b0c0d0e0f"
internal const val GCM_TAG_BITS = 128

class AesGcmConverter {

    private val secret: SecretKeySpec by lazy {
        val raw = MOCK_AES_KEY.hexToBytes()
        SecretKeySpec(raw, "AES")
    }

    @TypeConverter
    fun fromString(plain: String?): ByteArray? {
        if (plain == null) return null
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        val iv = ByteArray(12).also { SecureRandom().nextBytes(it) }
        cipher.init(Cipher.ENCRYPT_MODE, secret, GCMParameterSpec(GCM_TAG_BITS, iv))
        val enc = cipher.doFinal(plain.encodeToByteArray())
        return iv + enc // 前 12 字节是 IV
    }

    @TypeConverter
    fun toString(ciphertext: ByteArray?): String? {
        if (ciphertext == null || ciphertext.size < 12) return null
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        val iv = ciphertext.copyOfRange(0, 12)
        val data = ciphertext.copyOfRange(12, ciphertext.size)
        cipher.init(Cipher.DECRYPT_MODE, secret, GCMParameterSpec(GCM_TAG_BITS, iv))
        return cipher.doFinal(data).decodeToString()
    }
}

// ── 16 进制工具 ──────────────────────────────────
private fun String.hexToBytes(): ByteArray {
    val len = length / 2
    return ByteArray(len) { substring(it * 2, it * 2 + 2).toInt(16).toByte() }
}

// ── 数据库定义 ────────────────────────────────────
const val DB_NAME = "flowstate_audit.db"
const val DB_PASSPHRASE = "flowstate_ro"

@Database(
    entities = [HardwareAuditEntity::class, TrafficEntity::class],
    version = 1,
    exportSchema = false
)
@TypeConverters(AesGcmConverter::class)
abstract class FlowStateDatabase : RoomDatabase() {

    abstract fun hardwareAuditDao(): HardwareAuditDao
    abstract fun trafficDao(): TrafficDao

    companion object {
        @Volatile
        private var INSTANCE: FlowStateDatabase? = null

        // MOCK_DATA: isMock=true 时返回内存数据库
        fun getInstance(ctx: Context, isMock: Boolean = false):
                FlowStateDatabase {
            if (isMock) {
                // 内存 + 空密码,仅用于 UI 预览
                val factory = SupportFactory("mock".toByteArray())
                return Room.inMemoryDatabaseBuilder(
                    ctx, FlowStateDatabase::class.java
                ).openHelperFactory(factory)
                 .allowMainThreadQueries()
                 .build()
            }
            return INSTANCE ?: synchronized(this) {
                INSTANCE ?: buildProd(ctx).also { INSTANCE = it }
            }
        }

        private fun buildProd(ctx: Context): FlowStateDatabase {
            val factory = SupportFactory(DB_PASSPHRASE.toByteArray())
            return Room.databaseBuilder(
                ctx, FlowStateDatabase::class.java, DB_NAME
            ).openHelperFactory(factory)
             .build()
        }
    }
}
权限: INTERNETMOCK_DATA 注入点已标记 - TODO: 替换为 Real 实现