diff --git a/src/main/kotlin/com/github/saundefined/bitrix_idea/actions/CreateModuleAction.kt b/src/main/kotlin/com/github/saundefined/bitrix_idea/actions/CreateModuleAction.kt index f4047c9..cee5151 100644 --- a/src/main/kotlin/com/github/saundefined/bitrix_idea/actions/CreateModuleAction.kt +++ b/src/main/kotlin/com/github/saundefined/bitrix_idea/actions/CreateModuleAction.kt @@ -1,6 +1,8 @@ package com.github.saundefined.bitrix_idea.actions import com.github.saundefined.bitrix_idea.dialogs.CreateModuleDialogWrapper +import com.github.saundefined.bitrix_idea.license.CheckLicense +import com.github.saundefined.bitrix_idea.BitrixIdeaBundle.message import com.intellij.ide.util.DirectoryChooserUtil import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction @@ -11,6 +13,11 @@ import org.jetbrains.annotations.NotNull class CreateModuleAction : AnAction() { override fun actionPerformed(@NotNull event: AnActionEvent) { + if (CheckLicense.isLicensed == false) { + CheckLicense.requestLicense(message("license.request")) + return + } + val view = event.getData(LangDataKeys.IDE_VIEW) if (view === null) { return diff --git a/src/main/kotlin/com/github/saundefined/bitrix_idea/actions/CreateSimpleComponentAction.kt b/src/main/kotlin/com/github/saundefined/bitrix_idea/actions/CreateSimpleComponentAction.kt index 1c8157e..4699ee3 100644 --- a/src/main/kotlin/com/github/saundefined/bitrix_idea/actions/CreateSimpleComponentAction.kt +++ b/src/main/kotlin/com/github/saundefined/bitrix_idea/actions/CreateSimpleComponentAction.kt @@ -1,6 +1,8 @@ package com.github.saundefined.bitrix_idea.actions import com.github.saundefined.bitrix_idea.dialogs.CreateSimpleComponentDialogWrapper +import com.github.saundefined.bitrix_idea.license.CheckLicense +import com.github.saundefined.bitrix_idea.BitrixIdeaBundle.message import com.intellij.ide.util.DirectoryChooserUtil import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction @@ -9,9 +11,13 @@ import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.project.Project import org.jetbrains.annotations.NotNull - class CreateSimpleComponentAction : AnAction() { override fun actionPerformed(@NotNull event: AnActionEvent) { + if (CheckLicense.isLicensed == false) { + CheckLicense.requestLicense(message("license.request")) + return + } + val view = event.getData(LangDataKeys.IDE_VIEW) if (view === null) { return diff --git a/src/main/kotlin/com/github/saundefined/bitrix_idea/actions/CreateTemplateAction.kt b/src/main/kotlin/com/github/saundefined/bitrix_idea/actions/CreateTemplateAction.kt index 03222c6..28524f8 100644 --- a/src/main/kotlin/com/github/saundefined/bitrix_idea/actions/CreateTemplateAction.kt +++ b/src/main/kotlin/com/github/saundefined/bitrix_idea/actions/CreateTemplateAction.kt @@ -1,6 +1,8 @@ package com.github.saundefined.bitrix_idea.actions import com.github.saundefined.bitrix_idea.dialogs.CreateTemplateDialogWrapper +import com.github.saundefined.bitrix_idea.license.CheckLicense +import com.github.saundefined.bitrix_idea.BitrixIdeaBundle.message import com.intellij.ide.util.DirectoryChooserUtil import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction @@ -11,6 +13,11 @@ import org.jetbrains.annotations.NotNull class CreateTemplateAction : AnAction() { override fun actionPerformed(@NotNull event: AnActionEvent) { + if (CheckLicense.isLicensed == false) { + CheckLicense.requestLicense(message("license.request")) + return + } + val view = event.getData(LangDataKeys.IDE_VIEW) if (view === null) { return diff --git a/src/main/kotlin/com/github/saundefined/bitrix_idea/activity/NotificationStartupActivity.kt b/src/main/kotlin/com/github/saundefined/bitrix_idea/activity/NotificationStartupActivity.kt index 3a085bd..ecb6dae 100644 --- a/src/main/kotlin/com/github/saundefined/bitrix_idea/activity/NotificationStartupActivity.kt +++ b/src/main/kotlin/com/github/saundefined/bitrix_idea/activity/NotificationStartupActivity.kt @@ -1,6 +1,7 @@ package com.github.saundefined.bitrix_idea.activity import com.github.saundefined.bitrix_idea.BitrixIdeaBundle.message +import com.github.saundefined.bitrix_idea.license.CheckLicense import com.intellij.notification.* import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager @@ -22,29 +23,31 @@ class NotificationStartupActivity : StartupActivity { override fun runActivity(project: Project) { val app = ApplicationManager.getApplication() app.invokeLater { - val notification = - Notification(NOTIFICATION_GROUP, message("startup.notification.title"), NotificationType.IDE_UPDATE) - notification.addAction(object : - NotificationAction(message("startup.notification.action")) { - override fun actionPerformed(e: AnActionEvent, notification: Notification) { - openUrl(GITHUB_URL) - } - }) + if (CheckLicense.isLicensed == false) { + val notification = + Notification(NOTIFICATION_GROUP, message("startup.notification.title"), NotificationType.IDE_UPDATE) + notification.addAction(object : + NotificationAction(message("startup.notification.action")) { + override fun actionPerformed(e: AnActionEvent, notification: Notification) { + openUrl(GITHUB_URL) + } + }) - notification.addAction(object : NotificationAction(message("startup.notification.dismiss")) { - override fun actionPerformed(e: AnActionEvent, notification: Notification) { - val notificationSettings = NotificationsConfiguration.getNotificationsConfiguration() - notificationSettings.changeSettings( - NOTIFICATION_GROUP, - NotificationDisplayType.NONE, - false, - false - ) - notification.expire() - } - }) + notification.addAction(object : NotificationAction(message("startup.notification.dismiss")) { + override fun actionPerformed(e: AnActionEvent, notification: Notification) { + val notificationSettings = NotificationsConfiguration.getNotificationsConfiguration() + notificationSettings.changeSettings( + NOTIFICATION_GROUP, + NotificationDisplayType.NONE, + false, + false + ) + notification.expire() + } + }) - Notifications.Bus.notify(notification) + Notifications.Bus.notify(notification) + } } } diff --git a/src/main/kotlin/com/github/saundefined/bitrix_idea/license/CheckLicense.kt b/src/main/kotlin/com/github/saundefined/bitrix_idea/license/CheckLicense.kt new file mode 100644 index 0000000..4e3924e --- /dev/null +++ b/src/main/kotlin/com/github/saundefined/bitrix_idea/license/CheckLicense.kt @@ -0,0 +1,184 @@ +package com.github.saundefined.bitrix_idea.license + +import com.intellij.openapi.actionSystem.* +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState +import com.intellij.ui.LicensingFacade +import java.io.ByteArrayInputStream +import java.nio.charset.StandardCharsets +import java.security.Signature +import java.security.cert.* +import java.util.* + +object CheckLicense { + private val PRODUCT_CODE = "PBITRIXIDEA" + private val KEY_PREFIX = "key:" + private val STAMP_PREFIX = "stamp:" + private val EVAL_PREFIX = "eval:" + + private val ROOT_CERTIFICATES = arrayOf( + "-----BEGIN CERTIFICATE-----\n" + "MIIFOzCCAyOgAwIBAgIJANJssYOyg3nhMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV\n" + "BAMMDUpldFByb2ZpbGUgQ0EwHhcNMTUxMDAyMTEwMDU2WhcNNDUxMDI0MTEwMDU2\n" + "WjAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMIICIjANBgkqhkiG9w0BAQEFAAOC\n" + "Ag8AMIICCgKCAgEA0tQuEA8784NabB1+T2XBhpB+2P1qjewHiSajAV8dfIeWJOYG\n" + "y+ShXiuedj8rL8VCdU+yH7Ux/6IvTcT3nwM/E/3rjJIgLnbZNerFm15Eez+XpWBl\n" + "m5fDBJhEGhPc89Y31GpTzW0vCLmhJ44XwvYPntWxYISUrqeR3zoUQrCEp1C6mXNX\n" + "EpqIGIVbJ6JVa/YI+pwbfuP51o0ZtF2rzvgfPzKtkpYQ7m7KgA8g8ktRXyNrz8bo\n" + "iwg7RRPeqs4uL/RK8d2KLpgLqcAB9WDpcEQzPWegbDrFO1F3z4UVNH6hrMfOLGVA\n" + "xoiQhNFhZj6RumBXlPS0rmCOCkUkWrDr3l6Z3spUVgoeea+QdX682j6t7JnakaOw\n" + "jzwY777SrZoi9mFFpLVhfb4haq4IWyKSHR3/0BlWXgcgI6w6LXm+V+ZgLVDON52F\n" + "LcxnfftaBJz2yclEwBohq38rYEpb+28+JBvHJYqcZRaldHYLjjmb8XXvf2MyFeXr\n" + "SopYkdzCvzmiEJAewrEbPUaTllogUQmnv7Rv9sZ9jfdJ/cEn8e7GSGjHIbnjV2ZM\n" + "Q9vTpWjvsT/cqatbxzdBo/iEg5i9yohOC9aBfpIHPXFw+fEj7VLvktxZY6qThYXR\n" + "Rus1WErPgxDzVpNp+4gXovAYOxsZak5oTV74ynv1aQ93HSndGkKUE/qA/JECAwEA\n" + "AaOBhzCBhDAdBgNVHQ4EFgQUo562SGdCEjZBvW3gubSgUouX8bMwSAYDVR0jBEEw\n" + "P4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2Zp\n" + "bGUgQ0GCCQDSbLGDsoN54TAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkq\n" + "hkiG9w0BAQsFAAOCAgEAjrPAZ4xC7sNiSSqh69s3KJD3Ti4etaxcrSnD7r9rJYpK\n" + "BMviCKZRKFbLv+iaF5JK5QWuWdlgA37ol7mLeoF7aIA9b60Ag2OpgRICRG79QY7o\n" + "uLviF/yRMqm6yno7NYkGLd61e5Huu+BfT459MWG9RVkG/DY0sGfkyTHJS5xrjBV6\n" + "hjLG0lf3orwqOlqSNRmhvn9sMzwAP3ILLM5VJC5jNF1zAk0jrqKz64vuA8PLJZlL\n" + "S9TZJIYwdesCGfnN2AETvzf3qxLcGTF038zKOHUMnjZuFW1ba/12fDK5GJ4i5y+n\n" + "fDWVZVUDYOPUixEZ1cwzmf9Tx3hR8tRjMWQmHixcNC8XEkVfztID5XeHtDeQ+uPk\n" + "X+jTDXbRb+77BP6n41briXhm57AwUI3TqqJFvoiFyx5JvVWG3ZqlVaeU/U9e0gxn\n" + "8qyR+ZA3BGbtUSDDs8LDnE67URzK+L+q0F2BC758lSPNB2qsJeQ63bYyzf0du3wB\n" + "/gb2+xJijAvscU3KgNpkxfGklvJD/oDUIqZQAnNcHe7QEf8iG2WqaMJIyXZlW3me\n" + "0rn+cgvxHPt6N4EBh5GgNZR4l0eaFEV+fxVsydOQYo1RIyFMXtafFBqQl6DDxujl\n" + "FeU3FZ+Bcp12t7dlM4E0/sS1XdL47CfGVj4Bp+/VbF862HmkAbd7shs7sDQkHbU=\n" + "-----END CERTIFICATE-----\n", + ("-----BEGIN CERTIFICATE-----\n" + "MIIFTDCCAzSgAwIBAgIJAMCrW9HV+hjZMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV\n" + "BAMMEkxpY2Vuc2UgU2VydmVycyBDQTAgFw0xNjEwMTIxNDMwNTRaGA8yMTE2MTIy\n" + "NzE0MzA1NFowHTEbMBkGA1UEAwwSTGljZW5zZSBTZXJ2ZXJzIENBMIICIjANBgkq\n" + "hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoT7LvHj3JKK2pgc5f02z+xEiJDcvlBi6\n" + "fIwrg/504UaMx3xWXAE5CEPelFty+QPRJnTNnSxqKQQmg2s/5tMJpL9lzGwXaV7a\n" + "rrcsEDbzV4el5mIXUnk77Bm/QVv48s63iQqUjVmvjQt9SWG2J7+h6X3ICRvF1sQB\n" + "yeat/cO7tkpz1aXXbvbAws7/3dXLTgAZTAmBXWNEZHVUTcwSg2IziYxL8HRFOH0+\n" + "GMBhHqa0ySmF1UTnTV4atIXrvjpABsoUvGxw+qOO2qnwe6ENEFWFz1a7pryVOHXg\n" + "P+4JyPkI1hdAhAqT2kOKbTHvlXDMUaxAPlriOVw+vaIjIVlNHpBGhqTj1aqfJpLj\n" + "qfDFcuqQSI4O1W5tVPRNFrjr74nDwLDZnOF+oSy4E1/WhL85FfP3IeQAIHdswNMJ\n" + "y+RdkPZCfXzSUhBKRtiM+yjpIn5RBY+8z+9yeGocoxPf7l0or3YF4GUpud202zgy\n" + "Y3sJqEsZksB750M0hx+vMMC9GD5nkzm9BykJS25hZOSsRNhX9InPWYYIi6mFm8QA\n" + "2Dnv8wxAwt2tDNgqa0v/N8OxHglPcK/VO9kXrUBtwCIfZigO//N3hqzfRNbTv/ZO\n" + "k9lArqGtcu1hSa78U4fuu7lIHi+u5rgXbB6HMVT3g5GQ1L9xxT1xad76k2EGEi3F\n" + "9B+tSrvru70CAwEAAaOBjDCBiTAdBgNVHQ4EFgQUpsRiEz+uvh6TsQqurtwXMd4J\n" + "8VEwTQYDVR0jBEYwRIAUpsRiEz+uvh6TsQqurtwXMd4J8VGhIaQfMB0xGzAZBgNV\n" + "BAMMEkxpY2Vuc2UgU2VydmVycyBDQYIJAMCrW9HV+hjZMAwGA1UdEwQFMAMBAf8w\n" + "CwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQCJ9+GQWvBS3zsgPB+1PCVc\n" + "oG6FY87N6nb3ZgNTHrUMNYdo7FDeol2DSB4wh/6rsP9Z4FqVlpGkckB+QHCvqU+d\n" + "rYPe6QWHIb1kE8ftTnwapj/ZaBtF80NWUfYBER/9c6To5moW63O7q6cmKgaGk6zv\n" + "St2IhwNdTX0Q5cib9ytE4XROeVwPUn6RdU/+AVqSOspSMc1WQxkPVGRF7HPCoGhd\n" + "vqebbYhpahiMWfClEuv1I37gJaRtsoNpx3f/jleoC/vDvXjAznfO497YTf/GgSM2\n" + "LCnVtpPQQ2vQbOfTjaBYO2MpibQlYpbkbjkd5ZcO5U5PGrQpPFrWcylz7eUC3c05\n" + "UVeygGIthsA/0hMCioYz4UjWTgi9NQLbhVkfmVQ5lCVxTotyBzoubh3FBz+wq2Qt\n" + "iElsBrCMR7UwmIu79UYzmLGt3/gBdHxaImrT9SQ8uqzP5eit54LlGbvGekVdAL5l\n" + "DFwPcSB1IKauXZvi1DwFGPeemcSAndy+Uoqw5XGRqE6jBxS7XVI7/4BSMDDRBz1u\n" + "a+JMGZXS8yyYT+7HdsybfsZLvkVmc9zVSDI7/MjVPdk6h0sLn+vuPC1bIi5edoNy\n" + "PdiG2uPH5eDO6INcisyPpLS4yFKliaO4Jjap7yzLU9pbItoWgCAYa2NpxuxHJ0tB\n" + "7tlDFnvaRnQukqSG+VqNWg==\n" + "-----END CERTIFICATE-----") + ) + private val SECOND: Long = 1000 + private val MINUTE = 60 * SECOND + private val HOUR = 60 * MINUTE + private val TIMESTAMP_VALIDITY_PERIOD_MS = 1 * HOUR + val isLicensed: Boolean? + get() { + val facade = LicensingFacade.getInstance() ?: return null + val cstamp = facade.getConfirmationStamp(PRODUCT_CODE) ?: return false + if (cstamp.startsWith(KEY_PREFIX)) { + return isKeyValid(cstamp.substring(KEY_PREFIX.length)) + } + if (cstamp.startsWith(STAMP_PREFIX)) { + return isLicenseServerStampValid(cstamp.substring(STAMP_PREFIX.length)) + } + return if (cstamp.startsWith(EVAL_PREFIX)) { + isEvaluationValid(cstamp.substring(EVAL_PREFIX.length)) + } else false + } + + fun requestLicense(message: String) { + ApplicationManager.getApplication().invokeLater({ + showRegisterDialog( + PRODUCT_CODE, message + ) + }, ModalityState.NON_MODAL) + } + + private fun showRegisterDialog(productCode: String, message: String) { + val actionManager = ActionManager.getInstance() + var registerAction = actionManager.getAction("RegisterPlugins") + if (registerAction == null) { + registerAction = actionManager.getAction("Register") + } + registerAction?.actionPerformed( + AnActionEvent.createFromDataContext( + "", Presentation(), asDataContext(productCode, message) + ) + ) + } + + private fun asDataContext(productCode: String, message: String?): DataContext { + return DataContext { dataId: String? -> + when (dataId) { + "register.product-descriptor.code" -> return@DataContext productCode + "register.message" -> return@DataContext message + else -> return@DataContext null + } + } + } + + private fun isEvaluationValid(expirationTime: String): Boolean { + return try { + val now = Date() + val expiration = Date(expirationTime.toLong()) + now.before(expiration) + } catch (e: NumberFormatException) { + false + } + } + + private fun isKeyValid(key: String): Boolean { + val licenseParts = key.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (licenseParts.size != 4) { + return false + } + val licenseId = licenseParts[0] + val licensePartBase64 = licenseParts[1] + val signatureBase64 = licenseParts[2] + val certBase64 = licenseParts[3] + try { + val sig = Signature.getInstance("SHA1withRSA") + + sig.initVerify( + createCertificate( + Base64.getMimeDecoder().decode(certBase64.toByteArray(StandardCharsets.UTF_8)), emptySet(), false + ) + ) + val licenseBytes = Base64.getMimeDecoder().decode(licensePartBase64.toByteArray(StandardCharsets.UTF_8)) + sig.update(licenseBytes) + if (!sig.verify(Base64.getMimeDecoder().decode(signatureBase64.toByteArray(StandardCharsets.UTF_8)))) { + return false + } + + val licenseData = String(licenseBytes, StandardCharsets.UTF_8) + return licenseData.contains("\"licenseId\":\"$licenseId\"") + } catch (e: Throwable) { + e.printStackTrace() + } + return false + } + + private fun isLicenseServerStampValid(serverStamp: String): Boolean { + try { + val parts = serverStamp.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val base64 = Base64.getMimeDecoder() + val expectedMachineId = parts[0] + val timeStamp = parts[1].toLong() + val machineId = parts[2] + val signatureType = parts[3] + val signatureBytes = base64.decode(parts[4].toByteArray(StandardCharsets.UTF_8)) + val certBytes = base64.decode(parts[5].toByteArray(StandardCharsets.UTF_8)) + val intermediate: MutableCollection = ArrayList() + for (idx in 6 until parts.size) { + intermediate.add(base64.decode(parts[idx].toByteArray(StandardCharsets.UTF_8))) + } + val sig = Signature.getInstance(signatureType) + + sig.initVerify(createCertificate(certBytes, intermediate, true)) + sig.update(("$timeStamp:$machineId").toByteArray(StandardCharsets.UTF_8)) + if (sig.verify(signatureBytes)) { + return (expectedMachineId == machineId) && Math.abs(System.currentTimeMillis() - timeStamp) < TIMESTAMP_VALIDITY_PERIOD_MS + } + } catch (ignored: Throwable) { + + } + return false + } + + @Throws(Exception::class) + private fun createCertificate( + certBytes: ByteArray, intermediateCertsBytes: Collection, checkValidityAtCurrentDate: Boolean + ): X509Certificate { + val x509factory = CertificateFactory.getInstance("X.509") + val cert = x509factory.generateCertificate(ByteArrayInputStream(certBytes)) as X509Certificate + val allCerts: MutableCollection = HashSet() + allCerts.add(cert) + for (bytes: ByteArray in intermediateCertsBytes) { + allCerts.add(x509factory.generateCertificate(ByteArrayInputStream(bytes))) + } + try { + val selector = X509CertSelector() + selector.certificate = cert + + val trustAchors: MutableSet = HashSet() + for (rc: String in ROOT_CERTIFICATES) { + trustAchors.add( + TrustAnchor( + x509factory.generateCertificate(ByteArrayInputStream(rc.toByteArray(StandardCharsets.UTF_8))) as X509Certificate, + null + ) + ) + } + val pkixParams = PKIXBuilderParameters(trustAchors, selector) + pkixParams.isRevocationEnabled = false + if (!checkValidityAtCurrentDate) { + pkixParams.date = cert.notBefore + } + pkixParams.addCertStore( + CertStore.getInstance("Collection", CollectionCertStoreParameters(allCerts)) + ) + + val path = CertPathBuilder.getInstance("PKIX").build(pkixParams).certPath + if (path != null) { + CertPathValidator.getInstance("PKIX").validate(path, pkixParams) + return cert + } + } catch (e: Exception) { + + } + throw Exception("Certificate used to sign the license is not signed by JetBrains root certificate") + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e91bf45..1d649a8 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -3,6 +3,8 @@ Bitrix Idea sergey-panteleev + + com.intellij.modules.platform diff --git a/src/main/resources/messages/BitrixIdeaBundle.properties b/src/main/resources/messages/BitrixIdeaBundle.properties index 8aeb4d7..be3d389 100644 --- a/src/main/resources/messages/BitrixIdeaBundle.properties +++ b/src/main/resources/messages/BitrixIdeaBundle.properties @@ -33,4 +33,5 @@ localization.add=\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043B\u043E\u localization.add.title=\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u043B\u043E\u043A\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438 settings.localizations.group=\u041B\u043E\u043A\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044F \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E localization.table.column.name=\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u044F\u0437\u044B\u043A\u0438 -localization.table.row.name=\u041B\u043E\u043A\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044F: \ No newline at end of file +localization.table.row.name=\u041B\u043E\u043A\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044F: +license.request=Request a license