1package com.peachpasswords.sync23import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters4import org.bouncycastle.crypto.params.X25519PrivateKeyParameters567891011121314internal const val LOCAL_IDENTITY_RECORD_VERSION = 315161718192021internal const val LOCAL_IDENTITY_RECORD_VERSION_LEGACY_RANDOM = 22223internal const val FIELD_VERSION = "version"24internal const val FIELD_DEVICE_ID = "device_id"25internal const val FIELD_X25519_PRIVATE = "x25519_private"26internal const val FIELD_X25519_PUBLIC = "x25519_public"27internal const val FIELD_ED25519_SEED = "ed25519_seed"28internal const val FIELD_ED25519_PUBLIC = "ed25519_public"29internal const val FIELD_CREATED_AT = "created_at"3031private val HEX_64 = Regex("^[0-9a-fA-F]{64}$")3233internal sealed interface IdentityRecord {34 35 data object Absent : IdentityRecord3637 38 data class Current(39 val credentials: LocalIdentityCredentials,40 val createdAt: Long,41 ) : IdentityRecord4243 44 data class LegacyRandom(val credentials: LocalIdentityCredentials) : IdentityRecord4546 47 data object Invalid : IdentityRecord48}4950internal fun readIdentityRecord(fields: Map<String, Any?>): IdentityRecord {51 val deviceId = fields[FIELD_DEVICE_ID] as? String52 val xPrivateHex = fields[FIELD_X25519_PRIVATE] as? String53 val edSeedHex = fields[FIELD_ED25519_SEED] as? String54 val storedVersion = (fields[FIELD_VERSION] as? Number)?.toInt()5556 if (deviceId == null && xPrivateHex == null && edSeedHex == null) {57 return if (storedVersion == null) IdentityRecord.Absent else IdentityRecord.Invalid58 }59 if (storedVersion == null ||60 deviceId.isNullOrBlank() ||61 xPrivateHex == null || !HEX_64.matches(xPrivateHex) ||62 edSeedHex == null || !HEX_64.matches(edSeedHex)63 ) {64 return IdentityRecord.Invalid65 }6667 val xPrivate = hexToBytes(xPrivateHex)68 val edSeed = hexToBytes(edSeedHex)69 val credentials = LocalIdentityCredentials(70 deviceId = deviceId,71 x25519PrivateKey = xPrivate,72 x25519PublicKey = X25519PrivateKeyParameters(xPrivate, 0).generatePublicKey().encoded,73 ed25519Seed = edSeed,74 ed25519PublicKey = Ed25519PrivateKeyParameters(edSeed, 0).generatePublicKey().encoded,75 )7677 return when (storedVersion) {78 LOCAL_IDENTITY_RECORD_VERSION_LEGACY_RANDOM -> IdentityRecord.LegacyRandom(credentials)79 LOCAL_IDENTITY_RECORD_VERSION -> readFrozenRecord(fields, credentials)80 else -> IdentityRecord.Invalid81 }82}83848586878889private fun readFrozenRecord(90 fields: Map<String, Any?>,91 credentials: LocalIdentityCredentials,92): IdentityRecord {93 val xPublicHex = fields[FIELD_X25519_PUBLIC] as? String94 val edPublicHex = fields[FIELD_ED25519_PUBLIC] as? String95 val createdAt = (fields[FIELD_CREATED_AT] as? Number)?.toLong() ?: 0L96 if (xPublicHex == null || !HEX_64.matches(xPublicHex) ||97 edPublicHex == null || !HEX_64.matches(edPublicHex) ||98 createdAt <= 0L99 ) {100 return IdentityRecord.Invalid101 }102 if (!xPublicHex.equals(pubkeyToHex(credentials.x25519PublicKey), ignoreCase = true) ||103 !edPublicHex.equals(pubkeyToHex(credentials.ed25519PublicKey), ignoreCase = true)104 ) {105 return IdentityRecord.Invalid106 }107 return IdentityRecord.Current(credentials, createdAt)108}109110internal fun identityRecordFields(111 credentials: LocalIdentityCredentials,112 createdAt: Long,113): Map<String, Any?> = mapOf(114 FIELD_VERSION to LOCAL_IDENTITY_RECORD_VERSION,115 FIELD_DEVICE_ID to credentials.deviceId,116 FIELD_X25519_PRIVATE to pubkeyToHex(credentials.x25519PrivateKey),117 FIELD_X25519_PUBLIC to pubkeyToHex(credentials.x25519PublicKey),118 FIELD_ED25519_SEED to pubkeyToHex(credentials.ed25519Seed),119 FIELD_ED25519_PUBLIC to pubkeyToHex(credentials.ed25519PublicKey),120 FIELD_CREATED_AT to createdAt,121)