This commit is contained in:
2025-07-25 23:24:13 +08:00
parent e34bcd80dd
commit 4f366c12a3
84 changed files with 12608 additions and 2 deletions

View File

@@ -0,0 +1,212 @@
package fan.ovo.hwsign
import android.content.Context
import androidx.core.content.edit
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.IOException
import java.util.Base64
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class AuthService(private val context: Context) {
// Constants
private val baseUrl = "https://dbcs-api.ovo.fan"
private val jsonContentType = "application/json; charset=utf-8".toMediaType()
private val keyAuthToken = "auth_token"
// Key management
public val keyManager = KeyManager(context)
// Lazy initialization for network client
private val client by lazy {
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
private val sharedPreferences by lazy {
EncryptedSharedPreferences.create(
context,
"auth_prefs",
MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
/**
* Store and retrieve auth token from encrypted preferences
*/
private fun storeAuthToken(token: String) =
sharedPreferences.edit { putString(keyAuthToken, token) }
private fun getAuthToken(): String? =
sharedPreferences.getString(keyAuthToken, null)
/**
* Register a new user
*/
suspend fun register(username: String, password: String): Boolean =
withContext(Dispatchers.IO) {
try {
val json = JSONObject().apply {
put("username", username)
put("password", password)
}
val requestBody = json.toString().toRequestBody(jsonContentType)
val request = Request.Builder()
.url("$baseUrl/register")
.post(requestBody)
.build()
val response = client.newCall(request).await()
if (response.isSuccessful) {
response.body?.string()?.let { body ->
val jsonResponse = JSONObject(body)
return@withContext jsonResponse.optString("message", "").isNotEmpty()
}
}
false
} catch (e: Exception) {
e.printStackTrace()
false
}
}
/**
* Login using username/password and register hardware key
*/
suspend fun login(username: String, password: String): Boolean =
withContext(Dispatchers.IO) {
try {
// Generate new hardware key
val keyPair = keyManager.generateHardwareKey()
val publicKey = Base64.getEncoder().encodeToString(keyPair.public.encoded)
val json = JSONObject().apply {
put("username", username)
put("password", password)
}
val requestBody = json.toString().toRequestBody(jsonContentType)
val request = Request.Builder()
.url("$baseUrl/login")
.header("x-rpc-sec-bound-token-hw-pub", publicKey)
.header("x-rpc-sec-bound-token-hw-pub-type", "ecdsa")
.post(requestBody)
.build()
val response = client.newCall(request).await()
if (response.isSuccessful) {
response.body?.string()?.let { body ->
val jsonResponse = JSONObject(body)
val token = jsonResponse.optString("token", "")
if (token.isNotEmpty()) {
storeAuthToken(token)
// Clear any existing acceleration resources
keyManager.clearAccelKeyId()
return@withContext true
}
}
}
false
} catch (e: Exception) {
e.printStackTrace()
false
}
}
/**
* Check authentication status and manage acceleration keys
*/
suspend fun checkAuthentication(): Boolean = withContext(Dispatchers.IO) {
val token = getAuthToken() ?: return@withContext false
val timestamp = System.currentTimeMillis().toString()
try {
val accelKeyId = keyManager.getAccelKeyId()
val requestBuilder = Request.Builder()
.url("$baseUrl/authenticated")
.header("Authorization", "Bearer $token")
.header("x-rpc-sec-bound-token-data", timestamp)
if (accelKeyId != null) {
// Use existing acceleration key
val keyPair = keyManager.getOrCreateAccelerationKey()
val signature = keyManager.signWithKey(keyPair, timestamp)
requestBuilder
.header("x-rpc-sec-bound-token-data-sig", signature)
.header("x-rpc-sec-bound-token-accel-pub-id", accelKeyId)
} else {
// Generate a new acceleration key
val keyPair = keyManager.generateAccelerationKey()
val accelPubKey = Base64.getEncoder().encodeToString(keyPair.public.encoded)
val accelPubKeySig = keyManager.signWithHardwareKey(accelPubKey)
val signature = keyManager.signWithKey(keyPair, timestamp)
requestBuilder
.header("x-rpc-sec-bound-token-accel-pub", accelPubKey)
.header("x-rpc-sec-bound-token-accel-pub-type", "ecdsa")
.header("x-rpc-sec-bound-token-accel-pub-sig", accelPubKeySig)
.header("x-rpc-sec-bound-token-data-sig", signature)
}
val request = requestBuilder.get().build()
val response = client.newCall(request).await()
if (response.isSuccessful) {
// Save acceleration key ID if this was a new key registration
response.header("x-rpc-sec-bound-token-accel-pub-id")?.let { newId ->
keyManager.storeAccelKeyId(newId)
}
return@withContext true
}
false
} catch (e: Exception) {
e.printStackTrace()
false
}
}
/**
* Clear all stored credentials and keys
*/
fun logout() {
sharedPreferences.edit {
remove(keyAuthToken)
}
keyManager.clearKeys()
}
/**
* Extension function to simplify OkHttp async calls
*/
private suspend fun Call.await(): Response = suspendCoroutine { continuation ->
this.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
continuation.resume(response)
}
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e)
}
})
}
}

View File

@@ -0,0 +1,214 @@
package fan.ovo.hwsign
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.security.keystore.KeyInfo
import android.util.Log
import androidx.core.content.edit
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.security.KeyFactory
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.PrivateKey
import java.security.Signature
import java.security.spec.ECGenParameterSpec
import java.util.Base64
/**
* Manages cryptographic keys for hardware-backed authentication
*/
class KeyManager(private val context: Context) {
// Constants
private val keyAlias = "hw_sign_hardware_key"
private val keyAccelId = "accel_key_id"
// Cache for acceleration keys
private var accelerationKeyPair: KeyPair? = null
private val keystore by lazy {
KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
}
private val sharedPreferences by lazy {
EncryptedSharedPreferences.create(
context,
"auth_prefs",
MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
/**
* Check the security level of the hardware key
*/
@SuppressLint("SwitchIntDef")
fun getKeySecurityLevel(): String {
return try {
val privateKey = keystore.getKey(keyAlias, null) as PrivateKey
val keyFactory = KeyFactory.getInstance(privateKey.algorithm, "AndroidKeyStore")
val keyInfo = keyFactory.getKeySpec(privateKey, KeyInfo::class.java) as KeyInfo
// Acceptable value: StrongBox, TEE, SecureHardware.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
when (keyInfo.securityLevel) {
KeyProperties.SECURITY_LEVEL_STRONGBOX -> "StrongBox"
KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT -> "TEE"
KeyProperties.SECURITY_LEVEL_UNKNOWN_SECURE -> "UnknownSecure"
KeyProperties.SECURITY_LEVEL_SOFTWARE -> "Software"
KeyProperties.SECURITY_LEVEL_UNKNOWN -> "Unknown"
else -> "Unknown"
}
} else {
if (keyInfo.isInsideSecureHardware) {
"SecureHardware"
} else {
"Insecure"
}
}
} catch (e: Exception) {
e.printStackTrace()
"Unknown"
}
}
/**
* Generate a hardware-backed key pair using AndroidKeyStore
* Attempts to use StrongBox if available, falls back to TEE
*/
suspend fun generateHardwareKey(): KeyPair = withContext(Dispatchers.IO) {
// Delete any existing key
if (keystore.containsAlias(keyAlias)) {
keystore.deleteEntry(keyAlias)
}
val keyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"
)
val builder = KeyGenParameterSpec.Builder(
keyAlias, KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
).apply {
setDigests(KeyProperties.DIGEST_SHA256)
setUserAuthenticationRequired(false)
setUnlockedDeviceRequired(true)
}
var keyPair: KeyPair
// First try StrongBox if available (API 28+)
try {
builder.setIsStrongBoxBacked(true)
keyPairGenerator.initialize(builder.build())
keyPair = keyPairGenerator.generateKeyPair()
} catch (e: Exception) {
// Initialize with TEE fallback
builder.setIsStrongBoxBacked(false)
keyPairGenerator.initialize(builder.build())
keyPair = keyPairGenerator.generateKeyPair()
}
val keyLevel = getKeySecurityLevel()
Log.i(null, "successfully generated key pair in $keyLevel")
keyPair
}
/**
* Retrieve the existing hardware key pair from the Android KeyStore
*/
private fun getHardwareKeyPair(): KeyPair? = try {
if (keystore.containsAlias(keyAlias)) {
val privateKey = keystore.getKey(keyAlias, null) as PrivateKey
val publicKey = keystore.getCertificate(keyAlias).publicKey
KeyPair(publicKey, privateKey)
} else {
null
}
} catch (e: Exception) {
e.printStackTrace()
null
}
/**
* Store and retrieve acceleration key ID
*/
fun storeAccelKeyId(keyId: String) {
sharedPreferences.edit { putString(keyAccelId, keyId) }
}
fun getAccelKeyId(): String? =
sharedPreferences.getString(keyAccelId, null)
fun clearAccelKeyId() {
sharedPreferences.edit { remove(keyAccelId) }
}
/**
* Sign data using the hardware key
*/
suspend fun signWithHardwareKey(data: String): String = withContext(Dispatchers.IO) {
val keyPair = getHardwareKeyPair() ?: throw SecurityException("Hardware key not found")
val signature = Signature.getInstance("SHA256withECDSA").apply {
initSign(keyPair.private)
update(data.toByteArray())
}
Base64.getEncoder().encodeToString(signature.sign())
}
/**
* Generate an acceleration key pair in memory
*/
fun generateAccelerationKey(): KeyPair {
val keyPair = KeyPairGenerator.getInstance("EC").apply {
initialize(ECGenParameterSpec("secp256r1"))
}.generateKeyPair()
accelerationKeyPair = keyPair
return keyPair
}
/**
* Get the cached acceleration key pair or generate a new one
*/
fun getOrCreateAccelerationKey(): KeyPair {
return accelerationKeyPair ?: generateAccelerationKey()
}
/**
* Sign data with a specific key pair
*/
fun signWithKey(keyPair: KeyPair, data: String): String {
val signature = Signature.getInstance("SHA256withECDSA").apply {
initSign(keyPair.private)
update(data.toByteArray())
}
return Base64.getEncoder().encodeToString(signature.sign())
}
/**
* Clear stored keys and identifiers
*/
fun clearKeys() {
accelerationKeyPair = null
clearAccelKeyId()
try {
if (keystore.containsAlias(keyAlias)) {
keystore.deleteEntry(keyAlias)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View File

@@ -0,0 +1,115 @@
package fan.ovo.hwsign
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import fan.ovo.hwsign.ui.theme.DbcsTheme
class MainActivity : ComponentActivity() {
private lateinit var authService: AuthService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
authService = AuthService(this)
setContent {
DbcsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var message by remember { mutableStateOf("") }
var securityLevel by remember { mutableStateOf("Unknown") }
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("Username") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
lifecycleScope.launch {
val success = authService.login(username, password)
message = if (success) "Login successful!" else "Login failed."
securityLevel = authService.keyManager.getKeySecurityLevel()
}
}) {
Text("Login")
}
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = {
lifecycleScope.launch {
val success = authService.register(username, password)
message = if (success) "Registration successful!" else "Registration failed."
}
}) {
Text("Register")
}
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = {
lifecycleScope.launch {
val isAuthenticated = authService.checkAuthentication()
message = if (isAuthenticated) "Authenticated!" else "Not authenticated."
}
}) {
Text("Check Auth")
}
Spacer(modifier = Modifier.height(16.dp))
Text("Security Level: $securityLevel", style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.height(16.dp))
Text(message)
}
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
style = MaterialTheme.typography.titleLarge,
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
DbcsTheme {
Greeting("DBCS")
}
}

View File

@@ -0,0 +1,11 @@
package fan.ovo.hwsign.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@@ -0,0 +1,58 @@
package fan.ovo.hwsign.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun DbcsTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@@ -0,0 +1,25 @@
package fan.ovo.hwsign.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
)
)