package com.peachpasswords.sync import android.content.Context import java.util.UUID // The persisted keys keep their original names on purpose. Renaming them // would orphan this device's stored identity, which means a new device id and // a new sync keypair — every existing pairing would silently stop working on // upgrade. The name is stale; the cost of fixing it is not worth paying. private const val DEVICE_ID_KEY = "peach_chirp_device_id" private const val DEVICE_NAME_KEY = "peach_chirp_device_name" private const val PREFS_FILE = "peach_chirp_identity" data class LocalDeviceIdentity( val deviceId: String, val deviceName: String, val deviceType: DeviceType, val longTermPublicKeyHex: String, val signingPublicKeyHex: String, val wsPort: Int? = null, val relayUrl: String = Rendezvous.DEFAULT_RELAY_URL, /** * This device's signed Pro license blob (`PPK-…`), set only when Pro. Sent * in the outbound PAIR_IDENTITY so the peer can activate it after SAS — one * Pro device grants Pro to the other when pairing. Either direction. */ val license: String? = null, ) fun getOrCreateDeviceId(context: Context): String { val prefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE) val current = prefs.getString(DEVICE_ID_KEY, null) if (!current.isNullOrEmpty()) return current val fresh = UUID.randomUUID().toString() prefs.edit().putString(DEVICE_ID_KEY, fresh).apply() return fresh } fun getDeviceName(context: Context): String { val prefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE) return prefs.getString(DEVICE_NAME_KEY, null).orEmpty().ifBlank { defaultDeviceName() } } fun setDeviceName(context: Context, name: String) { val trimmed = name.trim() require(trimmed.isNotEmpty()) { "device name must not be empty" } require(trimmed.length <= 64) { "device name must be 64 chars or fewer" } context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE) .edit() .putString(DEVICE_NAME_KEY, trimmed) .apply() } fun clearDeviceIdentity(context: Context) { context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE) .edit() .clear() .apply() } fun buildLocalIdentity(context: Context, masterKey: ByteArray): LocalDeviceIdentity { val deviceId = getOrCreateDeviceId(context) val deviceName = getDeviceName(context) val kp = deriveSyncIdentityKeypair(masterKey, deviceId) val signing = deriveSyncSigningKeypair(masterKey, deviceId) return LocalDeviceIdentity( deviceId = deviceId, deviceName = deviceName, deviceType = extensionDeviceType(), longTermPublicKeyHex = pubkeyToHex(kp.publicKey), signingPublicKeyHex = pubkeyToHex(signing.publicKeyRaw), // No LAN port is advertised: peers reconnect through the relay // (docs/sync-lan-retirement.md), never by dialling this device. wsPort = null, relayUrl = RelaySettingsStore.readRelayUrl(context), ) } fun deriveLocalSyncKeypair(masterKey: ByteArray, deviceId: String): SyncIdentityKeypair { return deriveSyncIdentityKeypair(masterKey, deviceId) } private fun defaultDeviceName(): String { return "Android" } private fun extensionDeviceType(): DeviceType { return DeviceType.ANDROID }