package com.inou.clawdnode.security import android.content.Context import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.util.Base64 import android.util.Log import java.security.* import java.security.spec.ECGenParameterSpec /** * Manages device identity using Android Keystore. * * Generates and stores an EC P-256 keypair for signing Gateway challenge nonces. * The private key never leaves the secure hardware (TEE/SE) on supported devices. */ class DeviceIdentity(context: Context) { private val tag = "DeviceIdentity" private val keyAlias = "clawdnode_device_key" private val keyStore: KeyStore by lazy { KeyStore.getInstance("AndroidKeyStore").apply { load(null) } } /** * Get or generate the device's public key */ val publicKey: String by lazy { ensureKeyPair() val publicKey = keyStore.getCertificate(keyAlias)?.publicKey ?: throw IllegalStateException("No public key available") Base64.encodeToString(publicKey.encoded, Base64.NO_WRAP) } /** * Get device ID (fingerprint of the public key) */ val deviceId: String by lazy { ensureKeyPair() val publicKey = keyStore.getCertificate(keyAlias)?.publicKey ?: throw IllegalStateException("No public key available") // Create fingerprint using SHA-256 of the public key val digest = MessageDigest.getInstance("SHA-256") val hash = digest.digest(publicKey.encoded) // Return as hex string (first 16 bytes = 32 chars) hash.take(16).joinToString("") { "%02x".format(it) } } /** * Sign a challenge nonce for gateway authentication * * @param nonce The challenge nonce from the gateway * @return Signature and timestamp */ fun signChallenge(nonce: String): SignedChallenge { ensureKeyPair() val signedAt = System.currentTimeMillis() val dataToSign = "$nonce:$signedAt" val privateKey = keyStore.getKey(keyAlias, null) as? PrivateKey ?: throw IllegalStateException("No private key available") val signature = Signature.getInstance("SHA256withECDSA").apply { initSign(privateKey) update(dataToSign.toByteArray(Charsets.UTF_8)) } val signatureBytes = signature.sign() val signatureBase64 = Base64.encodeToString(signatureBytes, Base64.NO_WRAP) return SignedChallenge( signature = signatureBase64, signedAt = signedAt, nonce = nonce ) } /** * Ensure the keypair exists, generating if necessary */ private fun ensureKeyPair() { if (!keyStore.containsAlias(keyAlias)) { generateKeyPair() } } /** * Generate a new EC P-256 keypair in the Android Keystore */ private fun generateKeyPair() { Log.i(tag, "Generating new device keypair") val keyPairGenerator = KeyPairGenerator.getInstance( KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore" ) val parameterSpec = KeyGenParameterSpec.Builder( keyAlias, KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY ).apply { setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) setDigests(KeyProperties.DIGEST_SHA256) // Require user auth for extra security (optional, can be removed if too restrictive) // setUserAuthenticationRequired(true) // setUserAuthenticationValidityDurationSeconds(300) }.build() keyPairGenerator.initialize(parameterSpec) keyPairGenerator.generateKeyPair() Log.i(tag, "Device keypair generated successfully") } /** * Delete the keypair (for testing/reset) */ fun deleteKeyPair() { if (keyStore.containsAlias(keyAlias)) { keyStore.deleteEntry(keyAlias) Log.i(tag, "Device keypair deleted") } } data class SignedChallenge( val signature: String, val signedAt: Long, val nonce: String ) }