Привет, Хабр!
Сегодня мы хотим поделиться решением интересной и новой для нас задачи: нужно встроить поддержу ЭЦП в мобильное приложение заказчика.
Основные принципы и тезисы
Электронная цифровая подпись — это криптографический механизм, который обеспечивает:
Подлинность: позволяет удостовериться, что отправитель является именно тем, за кого себя выдает. В мире физических документов это аналогично проверке подписи на бумаге, которую может поставить только конкретный человек.
Целостность: гарантирует, что данные не были изменены после подписания. Если документ был изменен после подписания, подпись станет недействительной.
Безотказность: автор подписи не может впоследствии отказаться от своих действий, утверждая, что он не подписывал документ.
Всё это достигается благодаря использованию парных ключей: открытого и закрытого. Закрытый ключ хранится на защищенных носителях, а открытый распространяется вместе с данными, которые он подписывает.
Закрытый или Приватный ключ равносилен личной подписи от руки.
На этапе подписи данные хэшируются (то есть превращаются в уникальную «цифровую подпись»), которая шифруется закрытым ключом отправителя. Получатель может использовать открытый ключ отправителя, чтобы расшифровать подпись и сверить хэш с вновь сгенерированным из полученных данных. Несовпадение укажет на изменение данных.
Простой пример: Вы отправляете коробку с вложенным списком содержимого и запечатанную лентой с уникальным рисунком. Получатель открывает коробку, сверяет содержимое со списком и убеждается, что лента не повреждена.
Поскольку только отправитель может использовать свой закрытый ключ, чтобы создать подпись, он не может отказаться от авторства подписанного сообщения или транзакции; подписанная информация связывается именно с их идентичностью через уникальный ключ.
Простой пример: Ваша подпись на бумажном документе в силе до тех пор, пока никто другой не смог её подделать, что крайне маловероятно, как и кража закрытого ключа при правильном его хранении.
Ключи и сертификаты:
Закрытый ключ — это секретная часть пары ключей, которым подпись создается и который должен храниться в максимально защищенных условиях.
Открытый ключ — это общедоступная информация, необходимая для проверки подписи, связанная с вашим закрытым ключом.
Сертификат — это документ, удостоверяющий связь открытого ключа с именно тем, кому он принадлежит, подтвержденный надежным удостоверяющим центром (ЦС). Сертификат помогает другим доверять вашему открытому ключу.
Эта система позволяет поддерживать безопасность и доверие в цифровой коммуникации, что критически важно для мобильных приложений, обрабатывающих чувствительные и юридически значимые данные.
Внедрение ЭЦП в приложение
Теперь, когда мы вспомнили основные термины и принципы, давайте перейдем к рассмотрению нашего кейса более детально. Бизнес-процесс подразумевает формирование неких документов в мобильном приложении с дальнейшей их отправкой на сервер и последующей обработкой. Поскольку документы без подписи не имеют юридической силы, пользователям заказчика приходилось распечатывать созданные документы для их подписания вручную. Вот этот процесс и было решено автоматизировать с целью экономии времени и ресурсов.
После выяснения всех обстоятельств, средством доставки сертификатов и приватного ключа был выбран носитель Рутокен 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)
mishukovav
01.11.2024 10:42Интересная статья. Но как вы решаете вопрос с ЭП от ФНС - она же не экспортируемая с ключа и ключ никуда не вставить в телефоне. Как быть с этим - если все ЮЛ сейчас получают ЭП в налоговой.
PPR Автор
01.11.2024 10:42Добрый день! Спасибо за обратную связь!
Отвечаем на вопрос: если этот носитель будет формата Рутокен Лайт, то никаких проблем с копированием по нашему методу не будет. Рутокен Лайт является пассивным ключевым носителем и выступает в роли защищенного хранилища для извлекаемых ключей. В случае же носителей не экспортируемых ключей (например Рутокен ЭЦП 3.0) обязательным условием буде постоянное подключение носителя или прикладывания в случае с NFC носителем. Для подключения USB носителей ЭП к мобильному устройству приобретается любой OTG Переходник USB Type-C - USB
2128507
Ура. я дожил до того момента, как люди кодят асимметричную криптографию, и пытаются других учить, не понимая как она работает. А как я узнал? Да пример дебильный, с посылкой, и вообще дилетантство. Нормально, вполне в духе времени. Продолжайте, автор, плюсануть не могу, однако поддерживаю вас всеми руками, с такой конкуренцией я справлюсь.
едит: пример с подписью на бумажном документе тоже дебильный. Извините.
едит2: а вот это вобще тупизна тупизн: Закрытый или Приватный ключ равносилен личной подписи от руки
PPR Автор
Очень рады, что благодаря нашему материалу вы поверили в себя! Жаль, у вас нет своих публикаций, тоже бы поддержали всеми руками вашу минусовую карму
ShamsutOFF
Как же это здорово наверное, голословно оскорблять автора который старался.
Я бы с удовольствием почитал ваши высоконаучные и интеллектуальные статьи. Кинете ссылочку? Ой, а их нет что ли?
kacetal
Ну может человек просто конкурентов создавать не хочет и желает оставаться единственным специалистом по криптографии.