ChirpIdentity.kt

2.8 KB · raw

1package com.peachpasswords.sync23import android.content.Context4import java.util.UUID56private const val DEVICE_ID_KEY = "peach_chirp_device_id"7private const val DEVICE_NAME_KEY = "peach_chirp_device_name"8private const val PREFS_FILE = "peach_chirp_identity"910data class LocalChirpIdentity(11    val deviceId: String,12    val deviceName: String,13    val deviceType: DeviceType,14    val longTermPublicKeyHex: String,15    val signingPublicKeyHex: String,16    val wsPort: Int? = null,17    /**18     * This device's signed Pro license blob (`PPK-…`), set only when Pro. Sent19     * in the outbound PAIR_IDENTITY so the peer can activate it after SAS — one20     * Pro device grants Pro to the other when pairing. Either direction.21     */22    val license: String? = null,23)2425fun getOrCreateDeviceId(context: Context): String {26    val prefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)27    val current = prefs.getString(DEVICE_ID_KEY, null)28    if (!current.isNullOrEmpty()) return current29    val fresh = UUID.randomUUID().toString()30    prefs.edit().putString(DEVICE_ID_KEY, fresh).apply()31    return fresh32}3334fun getDeviceName(context: Context): String {35    val prefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)36    return prefs.getString(DEVICE_NAME_KEY, null).orEmpty().ifBlank { defaultDeviceName() }37}3839fun setDeviceName(context: Context, name: String) {40    val trimmed = name.trim()41    require(trimmed.isNotEmpty()) { "device name must not be empty" }42    require(trimmed.length <= 64) { "device name must be 64 chars or fewer" }43    context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)44        .edit()45        .putString(DEVICE_NAME_KEY, trimmed)46        .apply()47}4849fun buildLocalIdentity(context: Context, masterKey: ByteArray): LocalChirpIdentity {50    val deviceId = getOrCreateDeviceId(context)51    val deviceName = getDeviceName(context)52    val kp = deriveSyncIdentityKeypair(masterKey, deviceId)53    val signing = deriveSyncSigningKeypair(masterKey, deviceId)54    return LocalChirpIdentity(55        deviceId = deviceId,56        deviceName = deviceName,57        deviceType = extensionDeviceType(),58        longTermPublicKeyHex = pubkeyToHex(kp.publicKey),59        signingPublicKeyHex = pubkeyToHex(signing.publicKeyRaw),60        // Where peers should dial to reconnect without a pairing code. The61        // pairing window uses an ephemeral port and then closes, so a peer that62        // cached that one would never reach us again.63        wsPort = SyncReconnectListener.SYNC_PORT,64    )65}6667fun deriveLocalSyncKeypair(masterKey: ByteArray, deviceId: String): SyncIdentityKeypair {68    return deriveSyncIdentityKeypair(masterKey, deviceId)69}7071private fun defaultDeviceName(): String {72    return "Android"73}7475private fun extensionDeviceType(): DeviceType {76    return DeviceType.ANDROID77}