package com.peachpasswords.license import android.content.Context import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import dagger.hilt.android.qualifiers.ApplicationContext import java.math.BigInteger import javax.inject.Inject import javax.inject.Singleton import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters import org.bouncycastle.crypto.signers.Ed25519Signer @Singleton class LicenseRepository @Inject constructor(@ApplicationContext context: Context) { companion object { private const val PUBLIC_KEY_HEX = "c7dc52d2c484696cf8566dbe6cfe69025d114cb47f6cf53c25088ecfd539bdfa" private const val PREFS_FILE = "peach_license_prefs" private const val KEY_LICENSE = "peach_license" private const val KEY_PRO_CELEBRATED = "peach_pro_celebrated" private const val BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" const val PAYLOAD_SIZE = 12 const val SIGNATURE_SIZE = 64 const val TOTAL_SIZE = 76 const val PRODUCT_TAG = 0x01.toByte() const val PLAN_TAG = 0x01.toByte() const val VERSION_TAG = 0x01.toByte() } private val masterKey = MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() private val prefs = EncryptedSharedPreferences.create( context, PREFS_FILE, masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, ) fun storeLicense(key: String) { prefs.edit().putString(KEY_LICENSE, key).apply() prefs.edit().remove(KEY_PRO_CELEBRATED).apply() } fun getLicenseKey(): String? = prefs.getString(KEY_LICENSE, null) fun getLicenseIssuedAtMillis(): Long? = getLicenseKey()?.let(::licenseIssuedAtMillis) fun clearLicense() = prefs.edit().remove(KEY_LICENSE).apply() fun isProCelebrated(): Boolean = prefs.getBoolean(KEY_PRO_CELEBRATED, false) fun markProCelebrated() = prefs.edit().putBoolean(KEY_PRO_CELEBRATED, true).apply() fun isProActive(): Boolean = getLicenseKey()?.let(::validateLicense) == true fun hasFeature(feature: ProFeature): Boolean = isProActive() fun validateLicense(licenseKey: String): Boolean { return try { val decoded = decodeLicenseKey(licenseKey) ?: return false if (decoded.size != TOTAL_SIZE) return false val payload = decoded.copyOfRange(0, PAYLOAD_SIZE) val signature = decoded.copyOfRange(PAYLOAD_SIZE, TOTAL_SIZE) if (payload[0] != PRODUCT_TAG) return false if (payload[1] != PLAN_TAG) return false if (payload[2] != VERSION_TAG) return false verifySignature(payload, signature) } catch (_: Exception) { false } } fun licenseIssuedAtMillis(licenseKey: String): Long? { val decoded = decodeLicenseKey(licenseKey) ?: return null if (decoded.size != TOTAL_SIZE) return null val payload = decoded.copyOfRange(0, PAYLOAD_SIZE) if (payload[0] != PRODUCT_TAG || payload[1] != PLAN_TAG || payload[2] != VERSION_TAG) return null val days = (payload[3].toInt() and 0xff) or ((payload[4].toInt() and 0xff) shl 8) or ((payload[5].toInt() and 0xff) shl 16) or ((payload[6].toInt() and 0xff) shl 24) return days.toLong() * 86_400_000L } private fun decodeLicenseKey(key: String): ByteArray? { if (!key.startsWith("PPK-")) return null val encoded = key.removePrefix("PPK-").replace("-", "") return base58Decode(encoded) } private fun base58Decode(input: String): ByteArray? { return try { var n = BigInteger.ZERO for (char in input) { val index = BASE58_ALPHABET.indexOf(char) if (index < 0) return null n = n.multiply(BigInteger.valueOf(58)).add(BigInteger.valueOf(index.toLong())) } val bytes = n.toByteArray() if (bytes.isNotEmpty() && bytes[0] == 0.toByte()) bytes.copyOfRange(1, bytes.size) else bytes } catch (_: Exception) { null } } private fun verifySignature(payload: ByteArray, signature: ByteArray): Boolean { return try { val publicKeyBytes = hexToBytes(PUBLIC_KEY_HEX) val verifier = Ed25519Signer() verifier.init(false, Ed25519PublicKeyParameters(publicKeyBytes, 0)) verifier.update(payload, 0, payload.size) verifier.verifySignature(signature) } catch (_: Exception) { false } } private fun hexToBytes(hex: String): ByteArray { val len = hex.length val data = ByteArray(len / 2) for (i in 0 until len step 2) { data[i / 2] = ((Character.digit(hex[i], 16) shl 4) + Character.digit(hex[i + 1], 16)).toByte() } return data } }