DeviceIdentity.kt

3.1 KB · raw

1package com.peachpasswords.sync23import android.content.Context4import java.util.UUID56// The persisted keys keep their original names on purpose. Renaming them7// would orphan this device's stored identity, which means a new device id and8// a new sync keypair — every existing pairing would silently stop working on9// upgrade. The name is stale; the cost of fixing it is not worth paying.10private const val DEVICE_ID_KEY = "peach_chirp_device_id"11private const val DEVICE_NAME_KEY = "peach_chirp_device_name"12private const val PREFS_FILE = "peach_chirp_identity"1314data class LocalDeviceIdentity(15    val deviceId: String,16    val deviceName: String,17    val deviceType: DeviceType,18    val longTermPublicKeyHex: String,19    val signingPublicKeyHex: String,20    val wsPort: Int? = null,21    val relayUrl: String = Rendezvous.DEFAULT_RELAY_URL,22    /**23     * This device's signed Pro license blob (`PPK-…`), set only when Pro. Sent24     * in the outbound PAIR_IDENTITY so the peer can activate it after SAS — one25     * Pro device grants Pro to the other when pairing. Either direction.26     */27    val license: String? = null,28)2930fun getOrCreateDeviceId(context: Context): String {31    val prefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)32    val current = prefs.getString(DEVICE_ID_KEY, null)33    if (!current.isNullOrEmpty()) return current34    val fresh = UUID.randomUUID().toString()35    prefs.edit().putString(DEVICE_ID_KEY, fresh).apply()36    return fresh37}3839fun getDeviceName(context: Context): String {40    val prefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)41    return prefs.getString(DEVICE_NAME_KEY, null).orEmpty().ifBlank { defaultDeviceName() }42}4344fun setDeviceName(context: Context, name: String) {45    val trimmed = name.trim()46    require(trimmed.isNotEmpty()) { "device name must not be empty" }47    require(trimmed.length <= 64) { "device name must be 64 chars or fewer" }48    context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)49        .edit()50        .putString(DEVICE_NAME_KEY, trimmed)51        .apply()52}5354fun buildLocalIdentity(context: Context, masterKey: ByteArray): LocalDeviceIdentity {55    val deviceId = getOrCreateDeviceId(context)56    val deviceName = getDeviceName(context)57    val kp = deriveSyncIdentityKeypair(masterKey, deviceId)58    val signing = deriveSyncSigningKeypair(masterKey, deviceId)59    return LocalDeviceIdentity(60        deviceId = deviceId,61        deviceName = deviceName,62        deviceType = extensionDeviceType(),63        longTermPublicKeyHex = pubkeyToHex(kp.publicKey),64        signingPublicKeyHex = pubkeyToHex(signing.publicKeyRaw),65        // No LAN port is advertised: peers reconnect through the relay66        // (docs/sync-lan-retirement.md), never by dialling this device.67        wsPort = null,68        relayUrl = RelaySettingsStore.readRelayUrl(context),69    )70}7172fun deriveLocalSyncKeypair(masterKey: ByteArray, deviceId: String): SyncIdentityKeypair {73    return deriveSyncIdentityKeypair(masterKey, deviceId)74}7576private fun defaultDeviceName(): String {77    return "Android"78}7980private fun extensionDeviceType(): DeviceType {81    return DeviceType.ANDROID82}