Внедрение электронной цифровой подписи в мобильное приложение на Android

от автора

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

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

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

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

  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. Надеемся, что данная статья будет полезна всем, кто столкнется с подобной задачей!

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


ссылка на оригинал статьи https://habr.com/ru/articles/855314/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *