1package com.peachpasswords.license23import android.content.Context4import androidx.security.crypto.EncryptedSharedPreferences5import androidx.security.crypto.MasterKey6import dagger.hilt.android.qualifiers.ApplicationContext7import java.math.BigInteger8import java.security.KeyFactory9import java.security.Signature10import java.security.spec.X509EncodedKeySpec11import javax.inject.Inject12import javax.inject.Singleton1314@Singleton15class LicenseRepository @Inject constructor(@ApplicationContext context: Context) {16 companion object {17 private const val PUBLIC_KEY_HEX =18 "c7dc52d2c484696cf8566dbe6cfe69025d114cb47f6cf53c25088ecfd539bdfa"1920 private const val PREFS_FILE = "peach_license_prefs"21 private const val KEY_LICENSE = "peach_license"22 private const val KEY_PRO_CELEBRATED = "peach_pro_celebrated"2324 private const val BASE58_ALPHABET =25 "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"2627 const val PAYLOAD_SIZE = 1228 const val SIGNATURE_SIZE = 6429 const val TOTAL_SIZE = 763031 const val PRODUCT_TAG = 0x01.toByte()32 const val PLAN_TAG = 0x01.toByte()33 const val VERSION_TAG = 0x01.toByte()34 }3536 private val masterKey = MasterKey.Builder(context)37 .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)38 .build()3940 private val prefs = EncryptedSharedPreferences.create(41 context,42 PREFS_FILE,43 masterKey,44 EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,45 EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,46 )4748 fun storeLicense(key: String) {49 prefs.edit().putString(KEY_LICENSE, key).apply()50 prefs.edit().remove(KEY_PRO_CELEBRATED).apply()51 }5253 fun getLicenseKey(): String? = prefs.getString(KEY_LICENSE, null)5455 fun clearLicense() = prefs.edit().remove(KEY_LICENSE).apply()5657 fun isProCelebrated(): Boolean = prefs.getBoolean(KEY_PRO_CELEBRATED, false)5859 fun markProCelebrated() = prefs.edit().putBoolean(KEY_PRO_CELEBRATED, true).apply()6061 fun isProActive(): Boolean = getLicenseKey()?.let(::validateLicense) == true6263 fun hasFeature(feature: ProFeature): Boolean = isProActive()6465 fun validateLicense(licenseKey: String): Boolean {66 return try {67 val decoded = decodeLicenseKey(licenseKey) ?: return false68 if (decoded.size != TOTAL_SIZE) return false69 val payload = decoded.copyOfRange(0, PAYLOAD_SIZE)70 val signature = decoded.copyOfRange(PAYLOAD_SIZE, TOTAL_SIZE)71 if (payload[0] != PRODUCT_TAG) return false72 if (payload[1] != PLAN_TAG) return false73 if (payload[2] != VERSION_TAG) return false74 verifySignature(payload, signature)75 } catch (_: Exception) {76 false77 }78 }7980 private fun decodeLicenseKey(key: String): ByteArray? {81 if (!key.startsWith("PPK-")) return null82 val encoded = key.removePrefix("PPK-").replace("-", "")83 return base58Decode(encoded)84 }8586 private fun base58Decode(input: String): ByteArray? {87 return try {88 var n = BigInteger.ZERO89 for (char in input) {90 val index = BASE58_ALPHABET.indexOf(char)91 if (index < 0) return null92 n = n.multiply(BigInteger.valueOf(58)).add(BigInteger.valueOf(index.toLong()))93 }94 val bytes = n.toByteArray()95 if (bytes.isNotEmpty() && bytes[0] == 0.toByte()) bytes.copyOfRange(1, bytes.size) else bytes96 } catch (_: Exception) {97 null98 }99 }100101 private fun verifySignature(payload: ByteArray, signature: ByteArray): Boolean {102 return try {103 val publicKeyBytes = hexToBytes(PUBLIC_KEY_HEX)104 val keyFactory = KeyFactory.getInstance("Ed25519")105 val derPrefix = byteArrayOf(106 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65,107 0x70, 0x03, 0x21, 0x00,108 )109 val encoded = derPrefix + publicKeyBytes110 val publicKey = keyFactory.generatePublic(X509EncodedKeySpec(encoded))111 val sig = Signature.getInstance("Ed25519")112 sig.initVerify(publicKey)113 sig.update(payload)114 sig.verify(signature)115 } catch (_: Exception) {116 false117 }118 }119120 private fun hexToBytes(hex: String): ByteArray {121 val len = hex.length122 val data = ByteArray(len / 2)123 for (i in 0 until len step 2) {124 data[i / 2] = ((Character.digit(hex[i], 16) shl 4) +125 Character.digit(hex[i + 1], 16)).toByte()126 }127 return data128 }129}