mirror of
https://github.com/corda/corda.git
synced 2025-05-29 21:54:26 +00:00
Merge remote-tracking branch 'open/master' into mike-merge-4d2d9b83
This commit is contained in:
commit
9594aea9f7
@ -1373,8 +1373,6 @@ public final class net.corda.core.crypto.CordaSecurityProvider extends java.secu
|
|||||||
public static final class net.corda.core.crypto.CordaSecurityProvider$Companion extends java.lang.Object
|
public static final class net.corda.core.crypto.CordaSecurityProvider$Companion extends java.lang.Object
|
||||||
##
|
##
|
||||||
public final class net.corda.core.crypto.CordaSecurityProviderKt extends java.lang.Object
|
public final class net.corda.core.crypto.CordaSecurityProviderKt extends java.lang.Object
|
||||||
@NotNull
|
|
||||||
public static final String CORDA_SECURE_RANDOM_ALGORITHM = "CordaPRNG"
|
|
||||||
##
|
##
|
||||||
public final class net.corda.core.crypto.Crypto extends java.lang.Object
|
public final class net.corda.core.crypto.Crypto extends java.lang.Object
|
||||||
@NotNull
|
@NotNull
|
||||||
@ -1509,13 +1507,6 @@ public final class net.corda.core.crypto.CryptoUtils extends java.lang.Object
|
|||||||
public static final boolean verify(java.security.PublicKey, byte[], net.corda.core.crypto.DigitalSignature)
|
public static final boolean verify(java.security.PublicKey, byte[], net.corda.core.crypto.DigitalSignature)
|
||||||
public static final boolean verify(java.security.PublicKey, byte[], byte[])
|
public static final boolean verify(java.security.PublicKey, byte[], byte[])
|
||||||
##
|
##
|
||||||
public final class net.corda.core.crypto.DelegatingSecureRandomSpi extends java.security.SecureRandomSpi
|
|
||||||
public <init>(kotlin.jvm.functions.Function0<? extends java.security.SecureRandom>)
|
|
||||||
@Nullable
|
|
||||||
protected byte[] engineGenerateSeed(int)
|
|
||||||
protected void engineNextBytes(byte[])
|
|
||||||
protected void engineSetSeed(byte[])
|
|
||||||
##
|
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
public class net.corda.core.crypto.DigitalSignature extends net.corda.core.utilities.OpaqueBytes
|
public class net.corda.core.crypto.DigitalSignature extends net.corda.core.utilities.OpaqueBytes
|
||||||
public <init>(byte[])
|
public <init>(byte[])
|
||||||
|
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -165,6 +165,8 @@
|
|||||||
<module name="ha-testing_integrationTest" target="1.8" />
|
<module name="ha-testing_integrationTest" target="1.8" />
|
||||||
<module name="ha-testing_main" target="1.8" />
|
<module name="ha-testing_main" target="1.8" />
|
||||||
<module name="ha-testing_test" target="1.8" />
|
<module name="ha-testing_test" target="1.8" />
|
||||||
|
<module name="ha-utilities_main" target="1.8" />
|
||||||
|
<module name="ha-utilities_test" target="1.8" />
|
||||||
<module name="health-survey_main" target="1.8" />
|
<module name="health-survey_main" target="1.8" />
|
||||||
<module name="health-survey_test" target="1.8" />
|
<module name="health-survey_test" target="1.8" />
|
||||||
<module name="hsm-tool_main" target="1.8" />
|
<module name="hsm-tool_main" target="1.8" />
|
||||||
|
@ -64,6 +64,7 @@ see changes to this list.
|
|||||||
* Credit Suisse
|
* Credit Suisse
|
||||||
* cyrsis
|
* cyrsis
|
||||||
* Dan Newton (Accenture)
|
* Dan Newton (Accenture)
|
||||||
|
* Daniel Krajnik (BCS Technology International)
|
||||||
* Daniel Roig (SEB)
|
* Daniel Roig (SEB)
|
||||||
* Dave Hudson (R3)
|
* Dave Hudson (R3)
|
||||||
* David John Grundy (Dankse Bank)
|
* David John Grundy (Dankse Bank)
|
||||||
|
@ -77,7 +77,7 @@ buildscript {
|
|||||||
ext.snappy_version = '0.4'
|
ext.snappy_version = '0.4'
|
||||||
ext.class_graph_version = '4.2.12'
|
ext.class_graph_version = '4.2.12'
|
||||||
ext.jcabi_manifests_version = '1.1'
|
ext.jcabi_manifests_version = '1.1'
|
||||||
ext.picocli_version = '3.6.1'
|
ext.picocli_version = '3.8.0'
|
||||||
|
|
||||||
// Name of the IntelliJ SDK created for the deterministic Java rt.jar.
|
// Name of the IntelliJ SDK created for the deterministic Java rt.jar.
|
||||||
// ext.deterministic_idea_sdk = '1.8 (Deterministic)'
|
// ext.deterministic_idea_sdk = '1.8 (Deterministic)'
|
||||||
|
@ -72,7 +72,8 @@ object JacksonSupport {
|
|||||||
override val isFullParties: Boolean = false) : PartyObjectMapper, ObjectMapper(factory) {
|
override val isFullParties: Boolean = false) : PartyObjectMapper, ObjectMapper(factory) {
|
||||||
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = rpc.wellKnownPartyFromX500Name(name)
|
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = rpc.wellKnownPartyFromX500Name(name)
|
||||||
override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
|
override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
|
||||||
override fun partiesFromName(query: String) = rpc.partiesFromName(query, fuzzyIdentityMatch)
|
// Second parameter is exactMatch, so we have to invert the meaning here.
|
||||||
|
override fun partiesFromName(query: String) = rpc.partiesFromName(query, !fuzzyIdentityMatch)
|
||||||
override fun nodeInfoFromParty(party: AbstractParty): NodeInfo? = rpc.nodeInfoFromParty(party)
|
override fun nodeInfoFromParty(party: AbstractParty): NodeInfo? = rpc.nodeInfoFromParty(party)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
package net.corda.core.crypto
|
package net.corda.core.crypto
|
||||||
|
|
||||||
import io.netty.util.concurrent.FastThreadLocal
|
|
||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
import net.corda.core.StubOutForDJVM
|
import net.corda.core.StubOutForDJVM
|
||||||
import net.corda.core.crypto.CordaObjectIdentifier.COMPOSITE_KEY
|
import net.corda.core.crypto.CordaObjectIdentifier.COMPOSITE_KEY
|
||||||
import net.corda.core.crypto.CordaObjectIdentifier.COMPOSITE_SIGNATURE
|
import net.corda.core.crypto.CordaObjectIdentifier.COMPOSITE_SIGNATURE
|
||||||
import net.corda.core.internal.VisibleForTesting
|
import net.corda.core.crypto.internal.PlatformSecureRandomService
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||||
import java.security.Provider
|
import java.security.Provider
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.security.SecureRandomSpi
|
|
||||||
|
|
||||||
internal const val CORDA_SECURE_RANDOM_ALGORITHM = "CordaPRNG"
|
|
||||||
|
|
||||||
@KeepForDJVM
|
@KeepForDJVM
|
||||||
class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME security provider wrapper") {
|
class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME security provider wrapper") {
|
||||||
@ -24,8 +19,12 @@ class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME secur
|
|||||||
put("Signature.${CompositeSignature.SIGNATURE_ALGORITHM}", CompositeSignature::class.java.name)
|
put("Signature.${CompositeSignature.SIGNATURE_ALGORITHM}", CompositeSignature::class.java.name)
|
||||||
put("Alg.Alias.Signature.$COMPOSITE_SIGNATURE", CompositeSignature.SIGNATURE_ALGORITHM)
|
put("Alg.Alias.Signature.$COMPOSITE_SIGNATURE", CompositeSignature.SIGNATURE_ALGORITHM)
|
||||||
put("Alg.Alias.Signature.OID.$COMPOSITE_SIGNATURE", CompositeSignature.SIGNATURE_ALGORITHM)
|
put("Alg.Alias.Signature.OID.$COMPOSITE_SIGNATURE", CompositeSignature.SIGNATURE_ALGORITHM)
|
||||||
// Assuming this Provider is the first SecureRandom Provider, this algorithm is the SecureRandom default:
|
putPlatformSecureRandomService()
|
||||||
putService(DelegatingSecureRandomService(this))
|
}
|
||||||
|
|
||||||
|
@StubOutForDJVM
|
||||||
|
private fun putPlatformSecureRandomService() {
|
||||||
|
putService(PlatformSecureRandomService(this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,28 +49,3 @@ object CordaObjectIdentifier {
|
|||||||
@JvmField
|
@JvmField
|
||||||
val COMPOSITE_SIGNATURE = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003")
|
val COMPOSITE_SIGNATURE = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unlike all the NativePRNG algorithms, this doesn't use a global lock:
|
|
||||||
private class SunSecureRandom : SecureRandom(sun.security.provider.SecureRandom(), null)
|
|
||||||
|
|
||||||
private class DelegatingSecureRandomService(provider: CordaSecurityProvider) : Provider.Service(
|
|
||||||
provider, type, CORDA_SECURE_RANDOM_ALGORITHM, DelegatingSecureRandomSpi::class.java.name, null, null) {
|
|
||||||
private companion object {
|
|
||||||
private const val type = "SecureRandom"
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val instance = DelegatingSecureRandomSpi(::SunSecureRandom)
|
|
||||||
override fun newInstance(constructorParameter: Any?) = instance
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DelegatingSecureRandomSpi internal constructor(secureRandomFactory: () -> SecureRandom) : SecureRandomSpi() {
|
|
||||||
private val threadLocalSecureRandom = object : FastThreadLocal<SecureRandom>() {
|
|
||||||
override fun initialValue() = secureRandomFactory()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun engineSetSeed(seed: ByteArray) = threadLocalSecureRandom.get().setSeed(seed)
|
|
||||||
override fun engineNextBytes(bytes: ByteArray) = threadLocalSecureRandom.get().nextBytes(bytes)
|
|
||||||
override fun engineGenerateSeed(numBytes: Int): ByteArray? = threadLocalSecureRandom.get().generateSeed(numBytes)
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun currentThreadSecureRandom() = threadLocalSecureRandom.get()
|
|
||||||
}
|
|
||||||
|
@ -2,17 +2,52 @@
|
|||||||
@file:DeleteForDJVM
|
@file:DeleteForDJVM
|
||||||
package net.corda.core.crypto.internal
|
package net.corda.core.crypto.internal
|
||||||
|
|
||||||
|
import io.netty.util.concurrent.FastThreadLocal
|
||||||
import net.corda.core.DeleteForDJVM
|
import net.corda.core.DeleteForDJVM
|
||||||
import net.corda.core.crypto.CORDA_SECURE_RANDOM_ALGORITHM
|
|
||||||
import net.corda.core.crypto.DummySecureRandom
|
import net.corda.core.crypto.DummySecureRandom
|
||||||
import net.corda.core.utilities.SgxSupport
|
import net.corda.core.utilities.SgxSupport
|
||||||
|
import org.apache.commons.lang.SystemUtils
|
||||||
|
import java.security.Provider
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
import java.security.SecureRandomSpi
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This has been migrated into a separate class so that it
|
* This has been migrated into a separate class so that it
|
||||||
* is easier to delete from the core-deterministic module.
|
* is easier to delete from the core-deterministic module.
|
||||||
*/
|
*/
|
||||||
internal val platformSecureRandom = when {
|
internal val platformSecureRandom: () -> SecureRandom = when {
|
||||||
SgxSupport.isInsideEnclave -> DummySecureRandom
|
SgxSupport.isInsideEnclave -> { { DummySecureRandom } }
|
||||||
else -> SecureRandom.getInstance(CORDA_SECURE_RANDOM_ALGORITHM)
|
SystemUtils.IS_OS_LINUX -> {
|
||||||
|
{ SunSecureRandom() }
|
||||||
|
}
|
||||||
|
else -> SecureRandom::getInstanceStrong
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeleteForDJVM
|
||||||
|
class PlatformSecureRandomService(provider: Provider)
|
||||||
|
: Provider.Service(provider, "SecureRandom", algorithm, PlatformSecureRandomSpi::javaClass.name, null, null) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val algorithm = "CordaPRNG"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val instance: SecureRandomSpi = PlatformSecureRandomSpi()
|
||||||
|
override fun newInstance(constructorParameter: Any?) = instance
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteForDJVM
|
||||||
|
private class PlatformSecureRandomSpi : SecureRandomSpi() {
|
||||||
|
private val threadLocalSecureRandom = object : FastThreadLocal<SecureRandom>() {
|
||||||
|
override fun initialValue() = platformSecureRandom()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val secureRandom: SecureRandom = threadLocalSecureRandom.get()
|
||||||
|
|
||||||
|
override fun engineSetSeed(seed: ByteArray) = secureRandom.setSeed(seed)
|
||||||
|
override fun engineNextBytes(bytes: ByteArray) = secureRandom.nextBytes(bytes)
|
||||||
|
override fun engineGenerateSeed(numBytes: Int): ByteArray = secureRandom.generateSeed(numBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enterprise performance tweak: Unlike all the NativePRNG algorithms, this doesn't use a global lock:
|
||||||
|
// TODO: This is using private Java API. Just replace this with an implementation that always reads /dev/urandom on Linux.
|
||||||
|
private class SunSecureRandom : SecureRandom(sun.security.provider.SecureRandom(), null)
|
||||||
|
@ -18,12 +18,18 @@ import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider
|
|||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
|
|
||||||
internal val cordaSecurityProvider = CordaSecurityProvider().also {
|
val cordaSecurityProvider = CordaSecurityProvider().also {
|
||||||
|
// Among the others, we should register [CordaSecurityProvider] as the first provider, to ensure that when invoking [SecureRandom()]
|
||||||
|
// the [platformSecureRandom] is returned (which is registered in CordaSecurityProvider).
|
||||||
|
// Note that internally, [SecureRandom()] will look through all registered providers.
|
||||||
|
// Then it returns the first PRNG algorithm of the first provider that has registered a SecureRandom
|
||||||
|
// implementation (in our case [CordaSecurityProvider]), or null if none of the registered providers supplies
|
||||||
|
// a SecureRandom implementation.
|
||||||
Security.insertProviderAt(it, 1) // The position is 1-based.
|
Security.insertProviderAt(it, 1) // The position is 1-based.
|
||||||
}
|
}
|
||||||
// OID taken from https://tools.ietf.org/html/draft-ietf-curdle-pkix-00
|
// OID taken from https://tools.ietf.org/html/draft-ietf-curdle-pkix-00
|
||||||
internal val `id-Curve25519ph` = ASN1ObjectIdentifier("1.3.101.112")
|
val `id-Curve25519ph` = ASN1ObjectIdentifier("1.3.101.112")
|
||||||
internal val cordaBouncyCastleProvider = BouncyCastleProvider().apply {
|
val cordaBouncyCastleProvider = BouncyCastleProvider().apply {
|
||||||
putAll(EdDSASecurityProvider())
|
putAll(EdDSASecurityProvider())
|
||||||
// Override the normal EdDSA engine with one which can handle X509 keys.
|
// Override the normal EdDSA engine with one which can handle X509 keys.
|
||||||
put("Signature.${EdDSAEngine.SIGNATURE_ALGORITHM}", X509EdDSAEngine::class.java.name)
|
put("Signature.${EdDSAEngine.SIGNATURE_ALGORITHM}", X509EdDSAEngine::class.java.name)
|
||||||
@ -38,7 +44,7 @@ internal val cordaBouncyCastleProvider = BouncyCastleProvider().apply {
|
|||||||
// TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider.
|
// TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider.
|
||||||
Security.addProvider(it)
|
Security.addProvider(it)
|
||||||
}
|
}
|
||||||
internal val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply {
|
val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply {
|
||||||
require(name == "BCPQC") // The constant it comes from is not final.
|
require(name == "BCPQC") // The constant it comes from is not final.
|
||||||
}.also {
|
}.also {
|
||||||
Security.addProvider(it)
|
Security.addProvider(it)
|
||||||
@ -47,7 +53,7 @@ internal val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply {
|
|||||||
// that could cause unexpected and suspicious behaviour.
|
// that could cause unexpected and suspicious behaviour.
|
||||||
// i.e. if someone removes a Provider and then he/she adds a new one with the same name.
|
// i.e. if someone removes a Provider and then he/she adds a new one with the same name.
|
||||||
// The val is private to avoid any harmful state changes.
|
// The val is private to avoid any harmful state changes.
|
||||||
internal val providerMap = listOf(cordaBouncyCastleProvider, cordaSecurityProvider, bouncyCastlePQCProvider).map { it.name to it }.toMap()
|
val providerMap = listOf(cordaBouncyCastleProvider, cordaSecurityProvider, bouncyCastlePQCProvider).map { it.name to it }.toMap()
|
||||||
|
|
||||||
@DeleteForDJVM
|
@DeleteForDJVM
|
||||||
internal fun platformSecureRandomFactory(): SecureRandom = platformSecureRandom // To minimise diff of CryptoUtils against open-source.
|
fun platformSecureRandomFactory(): SecureRandom = platformSecureRandom() // To minimise diff of CryptoUtils against open-source.
|
||||||
|
@ -6,6 +6,7 @@ import net.corda.core.crypto.Crypto.ECDSA_SECP256R1_SHA256
|
|||||||
import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512
|
import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512
|
||||||
import net.corda.core.crypto.Crypto.RSA_SHA256
|
import net.corda.core.crypto.Crypto.RSA_SHA256
|
||||||
import net.corda.core.crypto.Crypto.SPHINCS256_SHA256
|
import net.corda.core.crypto.Crypto.SPHINCS256_SHA256
|
||||||
|
import net.corda.core.crypto.internal.PlatformSecureRandomService
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA
|
import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA
|
||||||
@ -21,10 +22,7 @@ import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
|
|||||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
|
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
|
||||||
import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY
|
import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY
|
||||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
|
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
|
||||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
|
||||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
|
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
|
||||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
|
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
|
||||||
import org.bouncycastle.jce.ECNamedCurveTable
|
import org.bouncycastle.jce.ECNamedCurveTable
|
||||||
@ -36,9 +34,9 @@ import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey
|
|||||||
import org.junit.Assert.assertNotEquals
|
import org.junit.Assert.assertNotEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.KeyPair
|
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.cert.X509Certificate
|
import java.security.SecureRandom
|
||||||
|
import java.security.Security
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.test.*
|
import kotlin.test.*
|
||||||
|
|
||||||
@ -948,26 +946,25 @@ class CryptoUtilsTest {
|
|||||||
this.outputStream.close()
|
this.outputStream.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createCert(signer: ContentSigner, keyPair: KeyPair): X509Certificate {
|
@Test
|
||||||
val dname = X500Name("CN=TestEntity")
|
fun `test default SecureRandom uses platformSecureRandom`() {
|
||||||
val startDate = Calendar.getInstance().let { cal ->
|
// Note than in Corda, [CordaSecurityProvider] is registered as the first provider.
|
||||||
cal.time = Date()
|
|
||||||
cal.add(Calendar.HOUR, -1)
|
// Remove [CordaSecurityProvider] in case it is already registered.
|
||||||
cal.time
|
Security.removeProvider(CordaSecurityProvider.PROVIDER_NAME)
|
||||||
}
|
// Try after removing CordaSecurityProvider.
|
||||||
val endDate = Calendar.getInstance().let { cal ->
|
val secureRandomNotRegisteredCordaProvider = SecureRandom()
|
||||||
cal.time = startDate
|
assertNotEquals(PlatformSecureRandomService.algorithm, secureRandomNotRegisteredCordaProvider.algorithm)
|
||||||
cal.add(Calendar.YEAR, 1)
|
|
||||||
cal.time
|
// Now register CordaSecurityProvider as last Provider.
|
||||||
}
|
Security.addProvider(CordaSecurityProvider())
|
||||||
val certificate = JcaX509v3CertificateBuilder(
|
val secureRandomRegisteredLastCordaProvider = SecureRandom()
|
||||||
dname,
|
assertNotEquals(PlatformSecureRandomService.algorithm, secureRandomRegisteredLastCordaProvider.algorithm)
|
||||||
BigInteger.TEN,
|
|
||||||
startDate,
|
// Remove Corda Provider again and add it as the first Provider entry.
|
||||||
endDate,
|
Security.removeProvider(CordaSecurityProvider.PROVIDER_NAME)
|
||||||
dname,
|
Security.insertProviderAt(CordaSecurityProvider(), 1) // This is base-1.
|
||||||
keyPair.public
|
val secureRandomRegisteredFirstCordaProvider = SecureRandom()
|
||||||
).build(signer)
|
assertEquals(PlatformSecureRandomService.algorithm, secureRandomRegisteredFirstCordaProvider.algorithm)
|
||||||
return JcaX509CertificateConverter().getCertificate(certificate)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,41 +1,9 @@
|
|||||||
package net.corda.core.crypto
|
package net.corda.core.crypto
|
||||||
|
|
||||||
import net.corda.core.crypto.internal.cordaSecurityProvider
|
|
||||||
import net.corda.core.internal.concurrent.fork
|
|
||||||
import net.corda.core.internal.join
|
|
||||||
import net.corda.core.utilities.getOrThrow
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertNotEquals
|
|
||||||
import kotlin.test.assertNotSame
|
|
||||||
import kotlin.test.assertSame
|
|
||||||
|
|
||||||
class SecureRandomTest {
|
class SecureRandomTest {
|
||||||
private companion object {
|
|
||||||
private val getSpi = SecureRandom::class.java.getDeclaredMethod("getSecureRandomSpi").apply { isAccessible = true }
|
|
||||||
private fun SecureRandom.spi() = getSpi.invoke(this)
|
|
||||||
|
|
||||||
init {
|
|
||||||
newSecureRandom() // Ensure all globals installed before running tests.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `newSecureRandom returns a global that delegates to thread-local`() {
|
|
||||||
val sr = newSecureRandom()
|
|
||||||
assertSame(sr, newSecureRandom())
|
|
||||||
checkDelegatesToThreadLocal(sr)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `regular SecureRandom delegates to thread-local`() {
|
|
||||||
val sr = SecureRandom()
|
|
||||||
assertSame(sr.spi(), SecureRandom().spi())
|
|
||||||
checkDelegatesToThreadLocal(sr)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout = 1000)
|
@Test(timeout = 1000)
|
||||||
fun `regular SecureRandom does not spend a lot of time seeding itself`() {
|
fun `regular SecureRandom does not spend a lot of time seeding itself`() {
|
||||||
val bytes = ByteArray(1000)
|
val bytes = ByteArray(1000)
|
||||||
@ -46,39 +14,4 @@ class SecureRandomTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `regular SecureRandom with seed delegates to thread-local`() {
|
|
||||||
val sr = SecureRandom(byteArrayOf(1, 2, 3))
|
|
||||||
assertSame(sr.spi(), SecureRandom(byteArrayOf(4, 5, 6)).spi())
|
|
||||||
checkDelegatesToThreadLocal(sr)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `SecureRandom#getInstance makes a SecureRandom that delegates to thread-local`() {
|
|
||||||
CORDA_SECURE_RANDOM_ALGORITHM.let {
|
|
||||||
val sr = SecureRandom.getInstance(it)
|
|
||||||
assertEquals(it, sr.algorithm)
|
|
||||||
assertSame(sr.spi(), SecureRandom.getInstance(it).spi())
|
|
||||||
checkDelegatesToThreadLocal(sr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkDelegatesToThreadLocal(sr: SecureRandom) {
|
|
||||||
val spi = sr.spi() as DelegatingSecureRandomSpi
|
|
||||||
val fg = spi.currentThreadSecureRandom()
|
|
||||||
val e = Executors.newSingleThreadExecutor()
|
|
||||||
val bg = e.fork(spi::currentThreadSecureRandom).getOrThrow()
|
|
||||||
assertNotSame(fg, bg) // Background thread got a distinct instance.
|
|
||||||
// Each thread always gets the same instance:
|
|
||||||
assertSame(fg, spi.currentThreadSecureRandom())
|
|
||||||
assertSame(bg, e.fork(spi::currentThreadSecureRandom).getOrThrow())
|
|
||||||
e.join()
|
|
||||||
assertSame(fg.provider, bg.provider)
|
|
||||||
assertNotSame(cordaSecurityProvider, fg.provider)
|
|
||||||
assertEquals(fg.algorithm, bg.algorithm)
|
|
||||||
assertNotEquals(CORDA_SECURE_RANDOM_ALGORITHM, fg.algorithm)
|
|
||||||
assertSame(cordaSecurityProvider, sr.provider)
|
|
||||||
assertEquals(CORDA_SECURE_RANDOM_ALGORITHM, sr.algorithm)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -188,8 +188,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
|||||||
// sum to more than the inputs. An issuance of zero size is not allowed.
|
// sum to more than the inputs. An issuance of zero size is not allowed.
|
||||||
//
|
//
|
||||||
// Note that this means literally anyone with access to the network can issue cash claims of arbitrary
|
// Note that this means literally anyone with access to the network can issue cash claims of arbitrary
|
||||||
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
|
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not.
|
||||||
// as-yet-unwritten identity service. See ADP-22 for discussion.
|
|
||||||
|
|
||||||
// The grouping ensures that all outputs have the same deposit reference and currency.
|
// The grouping ensures that all outputs have the same deposit reference and currency.
|
||||||
val inputAmount = inputs.sumCashOrZero(Issued(issuer, currency))
|
val inputAmount = inputs.sumCashOrZero(Issued(issuer, currency))
|
||||||
|
@ -18,6 +18,8 @@ import org.slf4j.LoggerFactory
|
|||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.security.auth.x500.X500Principal
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains utility methods for generating identities for a node.
|
* Contains utility methods for generating identities for a node.
|
||||||
@ -43,50 +45,67 @@ object DevIdentityGenerator {
|
|||||||
return identity.party
|
return identity.party
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateDistributedNotaryCompositeIdentity(dirs: List<Path>, notaryName: CordaX500Name, threshold: Int = 1): Party {
|
/** Generates a CFT notary identity, where the entire cluster shares a key pair. */
|
||||||
require(dirs.isNotEmpty())
|
|
||||||
|
|
||||||
log.trace { "Generating composite identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
|
|
||||||
val keyPairs = (1..dirs.size).map { generateKeyPair() }
|
|
||||||
val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold)
|
|
||||||
keyPairs.zip(dirs) { keyPair, nodeDir ->
|
|
||||||
generateCertificates(keyPair, notaryKey, notaryName, nodeDir)
|
|
||||||
}
|
|
||||||
return Party(notaryName, notaryKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateDistributedNotarySingularIdentity(dirs: List<Path>, notaryName: CordaX500Name): Party {
|
fun generateDistributedNotarySingularIdentity(dirs: List<Path>, notaryName: CordaX500Name): Party {
|
||||||
require(dirs.isNotEmpty())
|
require(dirs.isNotEmpty())
|
||||||
|
|
||||||
log.trace { "Generating singular identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
|
log.trace { "Generating singular identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
|
||||||
|
|
||||||
val keyPair = generateKeyPair()
|
val keyPair = generateKeyPair()
|
||||||
val notaryKey = keyPair.public
|
val notaryKey = keyPair.public
|
||||||
dirs.forEach { dir ->
|
|
||||||
generateCertificates(keyPair, notaryKey, notaryName, dir)
|
dirs.forEach { nodeDir ->
|
||||||
|
val keyStore = getKeyStore(nodeDir)
|
||||||
|
setPrivateKey(keyStore, keyPair, notaryName.x500Principal)
|
||||||
}
|
}
|
||||||
return Party(notaryName, notaryKey)
|
return Party(notaryName, notaryKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateCertificates(keyPair: KeyPair, notaryKey: PublicKey, notaryName: CordaX500Name, nodeDir: Path) {
|
/** Generates a BFT notary identity: individual key pairs for each cluster member, and a shared composite key. */
|
||||||
val (serviceKeyCert, compositeKeyCert) = listOf(keyPair.public, notaryKey).map { publicKey ->
|
fun generateDistributedNotaryCompositeIdentity(dirs: List<Path>, notaryName: CordaX500Name, threshold: Int = 1): Party {
|
||||||
X509Utilities.createCertificate(
|
require(dirs.isNotEmpty())
|
||||||
CertificateType.SERVICE_IDENTITY,
|
|
||||||
DEV_INTERMEDIATE_CA.certificate,
|
log.trace { "Generating composite identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
|
||||||
DEV_INTERMEDIATE_CA.keyPair,
|
|
||||||
notaryName.x500Principal,
|
val keyPairs = (1..dirs.size).map { generateKeyPair() }
|
||||||
publicKey)
|
val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold)
|
||||||
}
|
|
||||||
val distServKeyStoreFile = (nodeDir / "certificates").createDirectories() / "distributedService.jks"
|
keyPairs.zip(dirs) { keyPair, nodeDir ->
|
||||||
X509KeyStore.fromFile(distServKeyStoreFile, DEV_CA_KEY_STORE_PASS, createNew = true).update {
|
val keyStore = getKeyStore(nodeDir)
|
||||||
setCertificate("$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key", compositeKeyCert)
|
setPrivateKey(keyStore, keyPair, notaryName.x500Principal)
|
||||||
setPrivateKey(
|
setCompositeKey(keyStore, notaryKey, notaryName.x500Principal)
|
||||||
"$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key",
|
|
||||||
keyPair.private,
|
|
||||||
listOf(serviceKeyCert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate),
|
|
||||||
DEV_CA_KEY_STORE_PASS // Unfortunately we have to use the same password for private key due to Artemis limitation, for more details please see:
|
|
||||||
// org.apache.activemq.artemis.core.remoting.impl.ssl.SSLSupport.loadKeyManagerFactory
|
|
||||||
// where it is calling `KeyManagerFactory.init()` with store password
|
|
||||||
/*DEV_CA_PRIVATE_KEY_PASS*/)
|
|
||||||
}
|
}
|
||||||
|
return Party(notaryName, notaryKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getKeyStore(nodeDir: Path): X509KeyStore {
|
||||||
|
val distServKeyStoreFile = nodeDir / "certificates/distributedService.jks"
|
||||||
|
return X509KeyStore.fromFile(distServKeyStoreFile, DEV_CA_KEY_STORE_PASS, createNew = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setPrivateKey(keyStore: X509KeyStore, keyPair: KeyPair, notaryPrincipal: X500Principal) {
|
||||||
|
val serviceKeyCert = createCertificate(keyPair.public, notaryPrincipal)
|
||||||
|
keyStore.setPrivateKey(
|
||||||
|
"$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key",
|
||||||
|
keyPair.private,
|
||||||
|
listOf(serviceKeyCert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate),
|
||||||
|
DEV_CA_KEY_STORE_PASS // Unfortunately we have to use the same password for private key due to Artemis limitation, for more details please see:
|
||||||
|
// org.apache.activemq.artemis.core.remoting.impl.ssl.SSLSupport.loadKeyManagerFactory
|
||||||
|
// where it is calling `KeyManagerFactory.init()` with store password
|
||||||
|
/*DEV_CA_PRIVATE_KEY_PASS*/)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setCompositeKey(keyStore: X509KeyStore, compositeKey: PublicKey, notaryPrincipal: X500Principal) {
|
||||||
|
val compositeKeyCert = createCertificate(compositeKey, notaryPrincipal)
|
||||||
|
keyStore.setCertificate("$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key", compositeKeyCert)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCertificate(publicKey: PublicKey, principal: X500Principal): X509Certificate {
|
||||||
|
return X509Utilities.createCertificate(
|
||||||
|
CertificateType.SERVICE_IDENTITY,
|
||||||
|
DEV_INTERMEDIATE_CA.certificate,
|
||||||
|
DEV_INTERMEDIATE_CA.keyPair,
|
||||||
|
principal,
|
||||||
|
publicKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme
|
|||||||
import net.corda.serialization.internal.amqp.amqpMagic
|
import net.corda.serialization.internal.amqp.amqpMagic
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.nio.file.FileAlreadyExistsException
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -226,7 +227,13 @@ internal constructor(private val initSerEnv: Boolean,
|
|||||||
println("Copying CorDapp JARs into node directories")
|
println("Copying CorDapp JARs into node directories")
|
||||||
for (nodeDir in nodeDirs) {
|
for (nodeDir in nodeDirs) {
|
||||||
val cordappsDir = (nodeDir / "cordapps").createDirectories()
|
val cordappsDir = (nodeDir / "cordapps").createDirectories()
|
||||||
cordappJars.forEach { it.copyToDirectory(cordappsDir) }
|
cordappJars.forEach {
|
||||||
|
try {
|
||||||
|
it.copyToDirectory(cordappsDir)
|
||||||
|
} catch (e: FileAlreadyExistsException) {
|
||||||
|
println("WARNING: ${it.fileName} already exists in $cordappsDir, ignoring and leaving existing CorDapp untouched")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
generateServiceIdentitiesForNotaryClusters(configs)
|
generateServiceIdentitiesForNotaryClusters(configs)
|
||||||
|
@ -891,13 +891,17 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
val compositeKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key"
|
val compositeKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key"
|
||||||
|
|
||||||
val signingCertificateStore = configuration.signingCertificateStore.get()
|
val signingCertificateStore = configuration.signingCertificateStore.get()
|
||||||
|
// A composite key is only required for BFT notaries.
|
||||||
val certificates = if (cryptoService.containsKey(compositeKeyAlias)) {
|
val certificates = if (cryptoService.containsKey(compositeKeyAlias)) {
|
||||||
val certificate = signingCertificateStore[compositeKeyAlias]
|
val certificate = signingCertificateStore[compositeKeyAlias]
|
||||||
// We have to create the certificate chain for the composite key manually, this is because we don't have a keystore
|
// We have to create the certificate chain for the composite key manually, this is because we don't have a keystore
|
||||||
// provider that understand compositeKey-privateKey combo. The cert chain is created using the composite key certificate +
|
// provider that understand compositeKey-privateKey combo. The cert chain is created using the composite key certificate +
|
||||||
// the tail of the private key certificates, as they are both signed by the same certificate chain.
|
// the tail of the private key certificates, as they are both signed by the same certificate chain.
|
||||||
listOf(certificate) + signingCertificateStore.query { getCertificateChain(privateKeyAlias) }.drop(1)
|
listOf(certificate) + signingCertificateStore.query { getCertificateChain(privateKeyAlias) }.drop(1)
|
||||||
} else throw IllegalStateException("The identity public key for the notary service $serviceLegalName was not found in the key store.")
|
} else {
|
||||||
|
// We assume the notary is CFT, and each cluster member shares the same notary key pair.
|
||||||
|
signingCertificateStore.query { getCertificateChain(privateKeyAlias) }
|
||||||
|
}
|
||||||
|
|
||||||
val subject = CordaX500Name.build(certificates.first().subjectX500Principal)
|
val subject = CordaX500Name.build(certificates.first().subjectX500Principal)
|
||||||
if (subject != serviceLegalName) {
|
if (subject != serviceLegalName) {
|
||||||
|
@ -67,7 +67,7 @@ fun TestCordappImpl.packageAsJar(file: Path) {
|
|||||||
scanResult.use {
|
scanResult.use {
|
||||||
val manifest = createTestManifest(name, title, version, vendor, targetVersion)
|
val manifest = createTestManifest(name, title, version, vendor, targetVersion)
|
||||||
JarOutputStream(file.outputStream(), manifest).use { jos ->
|
JarOutputStream(file.outputStream(), manifest).use { jos ->
|
||||||
val time = FileTime.from(Instant.now())
|
val time = FileTime.from(Instant.EPOCH)
|
||||||
// The same resource may be found in different locations (this will happen when running from gradle) so just
|
// The same resource may be found in different locations (this will happen when running from gradle) so just
|
||||||
// pick the first one found.
|
// pick the first one found.
|
||||||
scanResult.allResources.asMap().forEach { path, resourceList ->
|
scanResult.allResources.asMap().forEach { path, resourceList ->
|
||||||
|
@ -114,31 +114,43 @@ class NodeTabView : Fragment() {
|
|||||||
|
|
||||||
fieldset("Additional configuration") {
|
fieldset("Additional configuration") {
|
||||||
styleClass.addAll("services-panel")
|
styleClass.addAll("services-panel")
|
||||||
val extraServices = if (nodeController.hasNotary()) {
|
if (nodeController.hasNotary()) {
|
||||||
listOf(USD, GBP, CHF, EUR).map { CurrencyIssuer(it) }
|
val extraServices: List<ExtraService> = listOf(USD, GBP, CHF, EUR).map { CurrencyIssuer(it) }
|
||||||
} else {
|
val servicesList = CheckListView(extraServices.observable()).apply {
|
||||||
listOf(NotaryService(true), NotaryService(false))
|
vboxConstraints { vGrow = Priority.ALWAYS }
|
||||||
}
|
model.item.extraServices.set(checkModel.checkedItems)
|
||||||
|
if (!nodeController.hasNotary()) {
|
||||||
val servicesList = CheckListView(extraServices.observable()).apply {
|
checkModel.check(0)
|
||||||
vboxConstraints { vGrow = Priority.ALWAYS }
|
checkModel.checkedItems.addListener(ListChangeListener { change ->
|
||||||
model.item.extraServices.set(checkModel.checkedItems)
|
while (change.next()) {
|
||||||
if (!nodeController.hasNotary()) {
|
if (change.wasAdded()) {
|
||||||
checkModel.check(0)
|
val item = change.addedSubList.last()
|
||||||
checkModel.checkedItems.addListener(ListChangeListener { change ->
|
val idx = checkModel.getItemIndex(item)
|
||||||
while (change.next()) {
|
checkModel.checkedIndices.forEach {
|
||||||
if (change.wasAdded()) {
|
if (it != idx) checkModel.clearCheck(it)
|
||||||
val item = change.addedSubList.last()
|
}
|
||||||
val idx = checkModel.getItemIndex(item)
|
|
||||||
checkModel.checkedIndices.forEach {
|
|
||||||
if (it != idx) checkModel.clearCheck(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
add(servicesList)
|
||||||
|
} else {
|
||||||
|
val notaryTypes = listOf(NotaryService(true), NotaryService(false))
|
||||||
|
val notaryTypeToggleGroup = togglegroup()
|
||||||
|
notaryTypeToggleGroup.selectedValueProperty<NotaryService>().addListener { observValue, oldValue, newValue ->
|
||||||
|
oldValue?.let {
|
||||||
|
model.item.extraServices.removeAll(it)
|
||||||
|
}
|
||||||
|
newValue?.let {
|
||||||
|
model.item.extraServices.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notaryTypes.forEachIndexed { index, notaryType ->
|
||||||
|
val toggle = radiobutton(notaryType.toString(), notaryTypeToggleGroup, notaryType)
|
||||||
|
toggle.isSelected = index == 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
add(servicesList)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ class InteractiveShellTest {
|
|||||||
constructor(party: Party) : this(party.name.toString())
|
constructor(party: Party) : this(party.name.toString())
|
||||||
constructor(b: Int?, amount: Amount<UserValue>) : this("${(b ?: 0) + amount.quantity} ${amount.token}")
|
constructor(b: Int?, amount: Amount<UserValue>) : this("${(b ?: 0) + amount.quantity} ${amount.token}")
|
||||||
constructor(b: Array<String>) : this(b.joinToString("+"))
|
constructor(b: Array<String>) : this(b.joinToString("+"))
|
||||||
constructor(amounts: Array<Amount<UserValue>>) : this(amounts.map(Amount<UserValue>::toString).joinToString("++"))
|
constructor(amounts: Array<Amount<UserValue>>) : this(amounts.joinToString("++", transform = Amount<UserValue>::toString))
|
||||||
|
|
||||||
override val progressTracker = ProgressTracker()
|
override val progressTracker = ProgressTracker()
|
||||||
override fun call() = a
|
override fun call() = a
|
||||||
|
Loading…
x
Reference in New Issue
Block a user