Привет, Хабр!

Сегодня мы хотим поделиться решением интересной и новой для нас задачи: нужно встроить поддержу ЭЦП в мобильное приложение заказчика.

Основные принципы и тезисы

Электронная цифровая подпись — это криптографический механизм, который обеспечивает:

  1. Подлинность: позволяет удостовериться, что отправитель является именно тем, за кого себя выдает. В мире физических документов это аналогично проверке подписи на бумаге, которую может поставить только конкретный человек.

  2. Целостность: гарантирует, что данные не были изменены после подписания. Если документ был изменен после подписания, подпись станет недействительной.

  3. Безотказность: автор подписи не может впоследствии отказаться от своих действий, утверждая, что он не подписывал документ.

Всё это достигается благодаря использованию парных ключей: открытого и закрытого. Закрытый ключ хранится на защищенных носителях, а открытый распространяется вместе с данными, которые он подписывает.

Закрытый или Приватный ключ равносилен личной подписи от руки.

На этапе подписи данные хэшируются (то есть превращаются в уникальную «цифровую подпись»), которая шифруется закрытым ключом отправителя. Получатель может использовать открытый ключ отправителя, чтобы расшифровать подпись и сверить хэш с вновь сгенерированным из полученных данных. Несовпадение укажет на изменение данных.

Простой пример: Вы отправляете коробку с вложенным списком содержимого и запечатанную лентой с уникальным рисунком. Получатель открывает коробку, сверяет содержимое со списком и убеждается, что лента не повреждена.

Поскольку только отправитель может использовать свой закрытый ключ, чтобы создать подпись, он не может отказаться от авторства подписанного сообщения или транзакции; подписанная информация связывается именно с их идентичностью через уникальный ключ.

Простой пример: Ваша подпись на бумажном документе в силе до тех пор, пока никто другой не смог её подделать, что крайне маловероятно, как и кража закрытого ключа при правильном его хранении.

Ключи и сертификаты:

  • Закрытый ключ — это секретная часть пары ключей, которым подпись создается и который должен храниться в максимально защищенных условиях.

  • Открытый ключ — это общедоступная информация, необходимая для проверки подписи, связанная с вашим закрытым ключом.

  • Сертификат — это документ, удостоверяющий связь открытого ключа с именно тем, кому он принадлежит, подтвержденный надежным удостоверяющим центром (ЦС). Сертификат помогает другим доверять вашему открытому ключу.

Эта система позволяет поддерживать безопасность и доверие в цифровой коммуникации, что критически важно для мобильных приложений, обрабатывающих чувствительные и юридически значимые данные.

Внедрение ЭЦП в приложение

Теперь, когда мы вспомнили основные термины и принципы, давайте перейдем к рассмотрению нашего кейса более детально. Бизнес-процесс подразумевает формирование неких документов в мобильном приложении с дальнейшей их отправкой на сервер и последующей обработкой. Поскольку документы без подписи не имеют юридической силы, пользователям заказчика приходилось распечатывать созданные документы для их подписания вручную. Вот этот процесс и было решено автоматизировать с целью экономии времени и ресурсов.

После выяснения всех обстоятельств, средством доставки сертификатов и приватного ключа был выбран носитель Рутокен Lite. Более детально со всем разнообразием подобных носителей и их отличий вы можете ознакомится на официальной странице техподдержки Рутокен. Провайдером же был выбран КриптоПро CSP. Такой выбор был сделан потому, что именно это сочетание обладает достаточной безопасностью, а также обеспечивает юридическую силу документов на территории Российской Федерации.

Для начала нам нужно организовать подключение к Рутокену. Делается это несложно. Сперва добавим библиотеку rtpcscbridge. Для этого пропишем такую зависимость в нашем build.gradle файле:

implementation 'ru.rutoken.rtpcscbridge:rtpcscbridge:1.2.0'

Также нам нужно проинициализировать зависимость. Для этого добавим следующие строки в метод onCreate() нашего Application,передадим контекст приложения и присоединим его к жизненному циклу:

RtPcscBridge.setAppContext(this)
RtPcscBridge.getTransportExtension().attachToLifecycle(this, true)

Далее нам необходимо подключить и настроить криптопровайдер для осуществления криптографических действий. Конкретно в нашем случае, это было копирование контейнера с ЭЦП на устройство, чтобы пользователям не приходилось постоянно держать Рутокен подключенным к устройству. Главная цель — подписание pdf-файлов. Разомнем пальцы и приступим.

Для начала настоятельно рекомендуем внимательно ознакомится с информацией по ссылке.

Если кратко, то мы скачиваем с официального сайта упомянутые в руководстве инструменты и примеры. В принципе, демонстрационное приложение от КриптоПро достаточно хорошо показывает широкий функционал возможностей для реализации. Однако, на наш взгляд, оно уже серьезно устарело, да и написано на Java, а у нас весь проект на Kotlin. Поэтому в данной статье мы будем приводить примеры обновленных нами функций, а оригиналы вы знаете, где найти.

Наша первая задача после сборки и настройки проекта — это правильно проинициализировать библиотеки криптошифрования. После успешной инициализации проверим наличие уже сохраненных контейнеров на устройстве. Согласно архитектуре нашего приложения, осуществить это было удобнее всего в UseCase.

init {
    initCSPProviders()
}


class InitError @JvmOverloads constructor(
    val errorCode: Int,
    val errorMessage: String? = null
) {
    companion object {
        const val INIT_JAVA_PROVIDER_ERROR: Int = 0xff
    }
}

// Инициализация CSP провайдеров
private fun initCSPProviders() {
    initCSPProvidersFuture = CompletableFuture.runAsync {
        val initCode = CSPConfig.init(context)
        if (initCode == CSPConfig.CSP_INIT_OK) {
            initJavaProviders(context, false)
        }
        initResult.postValue(InitError(initCode))
    }.thenRun {
        val mainHandler = Handler(Looper.getMainLooper())
        mainHandler.post {
            if (initResult.value?.errorCode == 0) {
                checkContainersOnDevice()
            }
        }
    }.exceptionally { throwable: Throwable ->
        Timber.e(
            throwable,
            "InitError(code = %s ,message = %s)",
            InitError.INIT_JAVA_PROVIDER_ERROR,
            throwable.message
        )
        initResult.postValue(
            InitError(
                InitError.INIT_JAVA_PROVIDER_ERROR,
                throwable.message
            )
        )
        null
    }
}

private fun initJavaProviders(context: Context, useSSPITlsProvider: Boolean) {
    if (Security.getProvider(JCSP.PROVIDER_NAME) == null)
        Security.addProvider(JCSP())

    Security.setProperty("ssl.KeyManagerFactory.algorithm", "GostX509")
    Security.setProperty("ssl.TrustManagerFactory.algorithm", "GostX509")
    Security.setProperty(
        "ssl.SocketFactory.provider",
        if (useSSPITlsProvider) "ru.CryptoPro.sspiSSL.SSLSocketFactoryImpl" else "ru.CryptoPro.ssl.SSLSocketFactoryImpl"
    )
    Security.setProperty(
        "ssl.ServerSocketFactory.provider",
        if (useSSPITlsProvider) "ru.CryptoPro.sspiSSL.SSLServerSocketFactoryImpl" else "ru.CryptoPro.ssl.SSLServerSocketFactoryImpl"
    )
    if (Security.getProvider("JTLS") == null) {
        if (useSSPITlsProvider) Security.addProvider(SSPISSL())
        else Security.addProvider(ru.CryptoPro.ssl.Provider())
    }

    cpSSLConfig.setDefaultSSLProvider(JCSP.PROVIDER_NAME)

    if (Security.getProvider(RevCheck.PROVIDER_NAME) == null)
        Security.addProvider(RevCheck())

    System.setProperty("ru.CryptoPro.CAdES.validate_tsp", "false")
    System.setProperty("com.sun.security.crl.timeout", "5")
    System.setProperty("ru.CryptoPro.crl.read_timeout", "5")
    AdESConfig.setDefaultProvider(JCSP.PROVIDER_NAME)
    System.setProperty("xml_xxe_protected", "false")
    XmlInit.init()
    ResourceResolver.registerAtStart(XmlInit.JCP_XML_DOCUMENT_ID_RESOLVER)
    val xmlDSigRi: Provider = XMLDSigRI()
    Security.addProvider(xmlDSigRi)
    val provider = Security.getProvider("XMLDSig")
    if (provider != null) {
        provider["XMLSignatureFactory.DOM"] =
            "ru.CryptoPro.JCPxml.dsig.internal.dom.DOMXMLSignatureFactory"
        provider["KeyInfoFactory.DOM"] =
            "ru.CryptoPro.JCPxml.dsig.internal.dom.DOMKeyInfoFactory"
    }
    System.setProperty("com.sun.security.enableCRLDP", "true")
    System.setProperty("com.ibm.security.enableCRLDP", "true")
    System.setProperty("disable_default_context", "true")
    System.setProperty("ngate_set_jcsp_if_gost", "true")
    System.setProperty("ru.CryptoPro.key_agreement_validation", "false")
    val trustStorePath = getBksTrustStore(context)
    val trustStorePassword = String(BKSTrustStore.STORAGE_PASSWORD)
    System.setProperty("javax.net.ssl.trustStoreType", BKSTrustStore.STORAGE_TYPE)
    System.setProperty("javax.net.ssl.trustStore", trustStorePath)
    System.setProperty("javax.net.ssl.trustStorePassword", trustStorePassword)
}

fun checkContainersOnDevice() {
    if (initResult.value!!.errorCode == 0) {
        val aliases =
            getAliasesOnStore(HDIMAGE, AlgorithmSelector.DefaultProviderType.pt2012Short)
        val updatedList = mutableListOf<CertificateDetails>()
        aliases.forEach { alias ->
            getCertificateDetails(alias = alias, storeType = HDIMAGE)?.let { cert ->
                if (userName == cert.subjectFIO)
                    updatedList.add(cert)
            }
        }
        if (updatedList.size == 1 && updatedList[0].validTo.after(Date()))
            setActiveCertificate(updatedList[0])
        certsOnDevice.postValue(updatedList)

        val activeAlias = sharedPref.getString("ACTIVE_SIGN_SP_$userId", null)
        if (activeAlias != null) {
            getCertificateDetails(alias = activeAlias, storeType = HDIMAGE)?.let { cert ->
                activeCertificate.postValue(cert)
            }
        }
    }
}
private fun getBksTrustStore(context: Context): String {
    return context.applicationInfo.dataDir + File.separator + BKSTrustStore.STORAGE_DIRECTORY + File.separator + BKSTrustStore.STORAGE_FILE_TRUST
}

fun clear() {
    initCSPProvidersFuture?.cancel(true)
}

После того, как провайдер готов к работе, добавляем инициализацию и слушателя на подключение носителя, чтобы мы могли считать и сохранить информацию с Рутокен:

private lateinit var rtTransport: RtTransport
private var readerObserver: RtTransport.PcscReaderObserver? = null

fun initRutoken() {
        try {
            rtTransport = RtPcscBridge.getTransport()
            rtTransport.initialize(context) 
            // Создаем и добавляем наблюдатель
            readerObserver = object : RtTransport.PcscReaderObserver {
                @RequiresApi(Build.VERSION_CODES.O)
                override fun onReaderAdded(reader: RtTransport.PcscReader) {
                    Timber.d("Reader added: ${reader.name}")
                    readRutoken(reader)
                }

                override fun onReaderRemoved(reader: RtTransport.PcscReader) {
                    Timber.d("Reader removed: ${reader.name}")
                }
            }
            rtTransport.addPcscReaderObserver(readerObserver!!)
        } catch (e: Exception) {
            Timber.e(e, "initRutoken Error")
        }
    }

	//Функция считывающая все алиасы контейнеров на носителе
    private fun readRutoken(reader: RtTransport.PcscReader) {
        val aliases = getAliasesOnStore(
            reader.name,
            AlgorithmSelector.DefaultProviderType.pt2012Short
        )
        aliases.forEach { alias ->
            val certDetailedInfo =
                getCertificateDetails(alias = alias, storeType = reader.name)
            if (certDetailedInfo != null) {
                activeCertificate.postValue(certDetailedInfo)

                if (userName == certDetailedInfo.subjectFIO) {
                    launch {
                        try {
                            onContainerCopyResult.postValue(
                                createContainerOnDevice(
                                    certDetailedInfo.alias,
                                    certDetailedInfo.privateKey!!,
                                    certDetailedInfo.certificate
                                )
                            )
                        } catch (e: Exception) {
                            Timber.e(e, "Ошибка копирования сертификата")
                            onContainerCopyResult.postValue("Ошибка копирования сертификата: ${e.message}")
                        }
                    }
                } else {
                    onContainerCopyResult.postValue(
                        "Нельзя сохранить сертификат. ФИО пользователя и владельца ЭЦП не совпадают.\n" +
                            "ФИО пользователя: ${inspectorService.cachedInspectorProfile?.user?.inspectorName}\n" +
                            "ФИО владельца ЭЦП: ${certDetailedInfo.subjectFIO}"
                    )
                }
            }
        }
    }

    fun stopRutoken() {
        readerObserver?.let { rtTransport.removePcscReaderObserver(it) }
        readerObserver = null
    }

Здесь мы инициализируем слушатель подключения носителя, а также считываем данные при его подключении с помощью библиотек КрипПро CSP и нескольких утилитарных функций, которые будут приведены ниже. Функция stopRutoken() останавливает мониторинг подключения, так как данный функционал нужен исключительно в одном месте приложения.

object KeyStoreUtil {

    private const val STR_CMS_OID_SIGNED = "1.2.840.113549.1.7.2";
    private const val STR_CMS_OID_DATA = "1.2.840.113549.1.7.1";

    private val providerType = AlgorithmSelector.DefaultProviderType.pt2012Short

    fun getAliasesOnStore(
        storeType: String,
        providerType: AlgorithmSelector.DefaultProviderType
    ): List<String> {
        val aliasesList = mutableListOf<String>()

        try {
            val keyStore = KeyStore.getInstance(storeType, JCSP.PROVIDER_NAME)
            keyStore.load(null, null)
            val aliases = keyStore.aliases()

            while (aliases.hasMoreElements()) {
                val alias = aliases.nextElement()
                val cert = keyStore.getCertificate(alias) as? X509Certificate
                val key = keyStore.getKey(alias, null)

                if (cert != null) {
                    val keyAlgorithm = cert.publicKey.algorithm

                    if (providerType == AlgorithmSelector.DefaultProviderType.pt2001 &&
                        keyAlgorithm.equals(JCP.GOST_EL_DEGREE_NAME, ignoreCase = true)
                    ) aliasesList.add(alias)
                    else if (providerType == AlgorithmSelector.DefaultProviderType.pt2012Short &&
                        keyAlgorithm.equals(JCP.GOST_EL_2012_256_NAME, ignoreCase = true)
                    ) aliasesList.add(alias)
                    else if (providerType == AlgorithmSelector.DefaultProviderType.pt2012Long &&
                        keyAlgorithm.equals(JCP.GOST_EL_2012_512_NAME, ignoreCase = true)
                    ) aliasesList.add(alias)

                }
            }
        } catch (e: Exception) {
            Timber.e(e, "getAliasesOnStore Error: ${e.message}")
        }

        return aliasesList
    }

    // Создает контейнер на устройстве и копирует в него ключи с Рутокена
    fun createContainerOnDevice(alias: String, privateKey: PrivateKey, certificate: Certificate):
        String {
        try {
            // Пароль для контейнера
            val password = "".toCharArray()
            val storeType = HDIMAGE

            if (checkAliasExists(alias, storeType)) {
                Timber.e("Container $alias already exists !!! ")
                return "Контейнер $alias уже был скопирован ранее!"
            }

            val keyStore = KeyStore.getInstance(storeType, JCSP.PROVIDER_NAME)
            keyStore.load(null, null)
            val entry = JCPPrivateKeyEntry(privateKey, arrayOf(certificate))
            val protectedParam = JCPProtectionParameter(password)
            keyStore.setEntry(alias, entry, protectedParam)
            if (keyStore.containsAlias(alias)) {
                Timber.i("Container created successfully with alias: $alias")
                getAliasesOnStore(storeType, providerType)
                return "Контейнер $alias успешно скопирован!"
            } else {
                Timber.e("Failed to create container with alias: $alias")
                return "Не удалось скопировать контейнер $alias!\n" +
                    "Пожалуйста, обратитесь к администратору!"
            }

        } catch (e: Exception) {
            Timber.e(e, "Error creating container on device")
            return "Не удалось скопировать контейнер $alias!\n" +
                "Ошибка: ${e.message}"
        }
    }

    private fun checkAliasExists(alias: String, storeType: String): Boolean {
        return getAliasesOnStore(storeType, providerType).contains(alias)
    }

    private fun extractInn(input: String): String? {
        // Регулярное выражение для поиска значения после "1.2.643.3.131.1.1="
        val regex = """1\.2\.643\.3\.131\.1\.1=([^,/]+)""".toRegex()
        val matchResult = regex.find(input)
        return matchResult?.groups?.get(1)?.value
    }

    private fun extractSnils(input: String): String? {
        // Регулярное выражение для поиска значения после "1.2.643.100.3="
        val regex = """1\.2\.643\.100\.3=([^,/]+)""".toRegex()
        val matchResult = regex.find(input)
        return matchResult?.groups?.get(1)?.value
    }

    private fun extractValue(docType: String, searchIn: String): String? {
        val regex =
            when (docType) {
                "СНИЛС" -> """1\.2\.643\.100\.3=([^,/]+)""".toRegex()
                "ИНН" -> """1\.2\.643\.3\.131\.1\.1=([^,/]+)""".toRegex()
                "ОГРН" -> """1\.2\.643\.100\.1=([^,/]+)""".toRegex()
                "ИННЮЛ" -> """1\.2\.643\.100\.4=([^,/]+)""".toRegex()
                "EMAILADDRESS" -> """EMAILADDRESS=([^,/]+)""".toRegex()
                else -> return null
            }
        val matchResult = regex.find(searchIn)
        return matchResult?.groups?.get(1)?.value
    }

    private fun getDocPattern(docType: String): Regex {
        return if (docType == "EMAILADDRESS")
            """EMAILADDRESS=#16[0-9A-Fa-f]+(?=,|$)""".toRegex()
        else
            """$docType=#12[0-9A-Fa-f]+(?=,|$)""".toRegex()
    }

    fun getCertificateDetails(alias: String, storeType: String): CertificateDetails? {
        try {
            val keyStore = KeyStore.getInstance(storeType, JCSP.PROVIDER_NAME)
            keyStore.load(null, null)
            val certificate = keyStore.getCertificate(alias) as? X509Certificate ?: return null
            val privateKey = keyStore.getKey(alias, null) as? PrivateKey ?: return null

            val certificateString = certificate.toString().trimIndent()

            val snils = "СНИЛС=${extractSnils(certificateString)}"
            val inn = "ИНН=${extractInn(certificateString)}"

            val subject = certificate.subjectDN.toString()
                .replace("OID.1.2.643.100.3", "СНИЛС")
                .replace("OID.1.2.643.3.131.1.1", "ИНН")
                .replace(getDocPattern("СНИЛС")) { snils }
                .replace(getDocPattern("ИНН")) { inn }
                .replace("EMAILADDRESS", "E")

            val ogrn = "ОГРН=${extractValue("ОГРН", certificateString)}"
            val innYl = "ИННЮЛ=${extractValue("ИННЮЛ", certificateString)}"
            val mail = "E=${extractValue("EMAILADDRESS", certificate.issuerDN.toString())}"

            val issuer = certificate.issuerDN.name.toString()
                .replace("1.2.643.100.1", "ОГРН")
                .replace("1.2.643.100.4", "ИННЮЛ")
                .replace("1.2.840.113549.1.9.1", "EMAILADDRESS")
                .replace(getDocPattern("ОГРН")) { ogrn }
                .replace(getDocPattern("ИННЮЛ")) { innYl }
                .replace(getDocPattern("EMAILADDRESS")) { mail }
                .replace(",", ", ")
                .replace("  ", " ")

            val serialNumber = certificate.serialNumber.toString(16).uppercase().padStart(34, '0')
            val signatureAlgorithm = certificate.sigAlgName
            val validFrom = certificate.notBefore
            val validTo = certificate.notAfter
            val publicKeyAlgorithm = certificate.publicKey.algorithm

            return CertificateDetails(
                alias = alias,
                certificate = certificate,
                privateKey = privateKey,
                issuer = issuer,
                subject = subject,
                subjectFIO = extractCommonName(subject),
                serialNumber = serialNumber,
                signatureAlgorithm = signatureAlgorithm,
                validFrom = validFrom,
                validTo = validTo,
                publicKeyAlgorithm = publicKeyAlgorithm
            )

        } catch (e: Exception) {
            Timber.e(e, "@@@ Error extracting certificate details for alias: $alias")
        }
        return null
    }

    private fun extractCommonName(subject: String): String {
        val regex = Regex("CN=([^,]+)")
        val matchResult = regex.find(subject)
        return matchResult?.groupValues?.get(1) ?: "Неизвестно"
    }

    @Throws(java.lang.Exception::class)
    fun createSign(
        dataForSign: ByteArray,
        keys: Array<PrivateKey>,
        certs: Array<Certificate>,
        providerType: AlgorithmSelector.DefaultProviderType
    ): ByteArray {

        val all = ContentInfo()
        all.contentType = Asn1ObjectIdentifier(
            OID(STR_CMS_OID_SIGNED).value
        )

        val cms = SignedData()
        all.content = cms
        cms.version = CMSVersion(1)


        val algorithmSelector = AlgorithmSelector.getInstance(providerType)
        cms.digestAlgorithms = DigestAlgorithmIdentifiers(1)
        val a = DigestAlgorithmIdentifier(
            OID(algorithmSelector.digestAlgorithmOid).value
        )
        a.parameters = Asn1Null()
        cms.digestAlgorithms.elements[0] = a

        cms.encapContentInfo = EncapsulatedContentInfo(
            Asn1ObjectIdentifier(
                OID(STR_CMS_OID_DATA).value
            ), null
        )

        // Сертификаты.

        val nCerts = certs.size
        cms.certificates = CertificateSet(nCerts)
        cms.certificates.elements = arrayOfNulls(nCerts)

        for (i in cms.certificates.elements.indices) {
            val certificate =
                ru.CryptoPro.JCP.ASN.PKIX1Explicit88.Certificate()

            val decodeBuffer =
                Asn1BerDecodeBuffer(certs[i].encoded)

            certificate.decode(decodeBuffer)
            cms.certificates.elements[i] = CertificateChoices()
            cms.certificates.elements[i].set_certificate(certificate)
        }


        val signature = Signature.getInstance(
            algorithmSelector.signatureAlgorithmName
        )

        var sign: ByteArray?

        // Подписанты (signerInfos).

        val nSigners = keys.size
        cms.signerInfos = SignerInfos(nSigners)
        for (i in cms.signerInfos.elements.indices) {

            cms.signerInfos.elements[i] = SignerInfo()
            cms.signerInfos.elements[i].version = CMSVersion(1)
            cms.signerInfos.elements[i].sid = SignerIdentifier()

            val encodedName = (certs[i] as X509Certificate)
                .issuerX500Principal.encoded

            val nameBuf =
                Asn1BerDecodeBuffer(encodedName)

            val name = Name()
            name.decode(nameBuf)

            val num = CertificateSerialNumber(
                (certs[i] as X509Certificate).serialNumber
            )
            cms.signerInfos.elements[i].sid.set_issuerAndSerialNumber(
                IssuerAndSerialNumber(name, num)
            )

            cms.signerInfos.elements[i].digestAlgorithm =
                DigestAlgorithmIdentifier(
                    OID(algorithmSelector.digestAlgorithmOid).value
                )

            cms.signerInfos.elements[i].digestAlgorithm.parameters = Asn1Null()

            val keyAlgOid = AlgorithmUtility.keyAlgToKeyAlgorithmOid(
                keys[i].algorithm
            ) // алгоритм ключа подписи

            cms.signerInfos.elements[i].signatureAlgorithm =
                SignatureAlgorithmIdentifier(OID(keyAlgOid).value)

            cms.signerInfos.elements[i].signatureAlgorithm.parameters = Asn1Null()

            val data2hash: ByteArray = dataForSign

            signature.initSign(keys[i])
            signature.update(data2hash)
            sign = signature.sign()

            cms.signerInfos.elements[i].signature = SignatureValue(sign)
        }


        // CMS подпись.
        val asnBuf = Asn1BerEncodeBuffer()
        all.encode(asnBuf, true)
        val sig = asnBuf.msgCopy
        return sig
    }
}
/**
 * Служебный класс AlgorithmSelector предназначен
 * для получения алгоритмов и свойств, соответствующих
 * заданному провайдеру.
 */
open class AlgorithmSelector protected constructor(
    val providerType: DefaultProviderType,
    val signatureAlgorithmName: String,
    val digestAlgorithmName: String,
    val digestAlgorithmOid: String
) {
    /**
     * Возможные типы провайдеров.
     */
    enum class DefaultProviderType { pt2001, pt2012Short, pt2012Long }

    companion object {
        /**
         * Получение списка алгоритмов для данного провайдера.
         *
         * @param pt Тип провайдера.
         * @return настройки провайдера.
         */
        fun getInstance(pt: DefaultProviderType): AlgorithmSelector {
            Timber.d("@@@ getInstance($pt)")
            return when (pt) {
                DefaultProviderType.pt2001 -> AlgorithmSelector_2011()
                DefaultProviderType.pt2012Short -> AlgorithmSelector_2012_256()
                DefaultProviderType.pt2012Long -> AlgorithmSelector_2012_512()
            }
        }

        /**
         * Получение типа провайдера по его строковому представлению.
         *
         * @param value Тип в виде числа.
         * @return тип в виде значения из перечисления.
         */
        @JvmStatic
        fun find(value: Int): DefaultProviderType {
            Timber.d("@@@ find($value)")
            return when (value) {
                0 -> DefaultProviderType.pt2001
                1 -> DefaultProviderType.pt2012Short
                2 -> DefaultProviderType.pt2012Long
                else -> throw IllegalArgumentException("Unknown value")
            }
        }
    }
}
//------------------------------------------------------------------------------------------------------------------

/**
 * Класс с алгоритмами ГОСТ 2001.
 */
private class AlgorithmSelector_2011 : AlgorithmSelector(
    DefaultProviderType.pt2001,
    JCP.GOST_EL_SIGN_NAME,
    JCP.GOST_DIGEST_NAME,
    JCP.GOST_DIGEST_OID
)

/**
 * Класс с алгоритмами ГОСТ 2012 (256).
 */
private class AlgorithmSelector_2012_256 : AlgorithmSelector(
    DefaultProviderType.pt2012Short,
    JCP.GOST_SIGN_2012_256_NAME,
    JCP.GOST_DIGEST_2012_256_NAME,
    JCP.GOST_DIGEST_2012_256_OID
)

/**
 * Класс с алгоритмами ГОСТ 2012 (512).
 */
private class AlgorithmSelector_2012_512 : AlgorithmSelector(
    DefaultProviderType.pt2012Long,
    JCP.GOST_SIGN_2012_512_NAME,
    JCP.GOST_DIGEST_2012_512_NAME,
    JCP.GOST_DIGEST_2012_512_OID
)

На этом всё, ЭЦП успешно внедрена в мобильное приложение на Android. Надеемся, что данная статья будет полезна всем, кто столкнется с подобной задачей!

Удачи в разработке!

Комментарии (6)


  1. 2128507
    01.11.2024 10:42

    Ура. я дожил до того момента, как люди кодят асимметричную криптографию, и пытаются других учить, не понимая как она работает. А как я узнал? Да пример дебильный, с посылкой, и вообще дилетантство. Нормально, вполне в духе времени. Продолжайте, автор, плюсануть не могу, однако поддерживаю вас всеми руками, с такой конкуренцией я справлюсь.

    едит: пример с подписью на бумажном документе тоже дебильный. Извините.

    едит2: а вот это вобще тупизна тупизн: Закрытый или Приватный ключ равносилен личной подписи от руки


    1. PPR Автор
      01.11.2024 10:42

      Очень рады, что благодаря нашему материалу вы поверили в себя! Жаль, у вас нет своих публикаций, тоже бы поддержали всеми руками вашу минусовую карму


    1. ShamsutOFF
      01.11.2024 10:42

      Как же это здорово наверное, голословно оскорблять автора который старался.
      Я бы с удовольствием почитал ваши высоконаучные и интеллектуальные статьи. Кинете ссылочку? Ой, а их нет что ли?


      1. kacetal
        01.11.2024 10:42

        Ну может человек просто конкурентов создавать не хочет и желает оставаться единственным специалистом по криптографии.


  1. mishukovav
    01.11.2024 10:42

    Интересная статья. Но как вы решаете вопрос с ЭП от ФНС - она же не экспортируемая с ключа и ключ никуда не вставить в телефоне. Как быть с этим - если все ЮЛ сейчас получают ЭП в налоговой.


    1. PPR Автор
      01.11.2024 10:42

      Добрый день! Спасибо за обратную связь!

      Отвечаем на вопрос: если этот носитель будет формата Рутокен Лайт, то никаких проблем с копированием по нашему методу не будет. Рутокен Лайт является пассивным ключевым носителем и выступает в роли защищенного хранилища для извлекаемых ключей. В случае же носителей не экспортируемых ключей (например  Рутокен ЭЦП 3.0) обязательным условием буде постоянное подключение носителя или прикладывания в случае с NFC носителем. Для подключения USB носителей ЭП к мобильному устройству приобретается любой OTG Переходник USB Type-C - USB