diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 00df418c..07194d19 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -6,6 +6,7 @@ plugins { dependencies { api("com.google.inject:guice:${Versions.guice}") api("io.netty:netty-buffer:${Versions.netty}") + api("org.bouncycastle:bcpkix-jdk15on:${Versions.bouncyCastle}") api("org.bouncycastle:bcprov-jdk15on:${Versions.bouncyCastle}") testImplementation("com.google.jimfs:jimfs:${Versions.jimfs}") diff --git a/common/src/main/java/dev/openrs2/common/crypto/Pkcs12KeyStore.kt b/common/src/main/java/dev/openrs2/common/crypto/Pkcs12KeyStore.kt index b68dfc41..27a6d6c7 100644 --- a/common/src/main/java/dev/openrs2/common/crypto/Pkcs12KeyStore.kt +++ b/common/src/main/java/dev/openrs2/common/crypto/Pkcs12KeyStore.kt @@ -1,8 +1,24 @@ package dev.openrs2.common.crypto +import org.bouncycastle.asn1.nist.NISTObjectIdentifiers +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers +import org.bouncycastle.asn1.x500.X500NameBuilder +import org.bouncycastle.asn1.x500.style.BCStyle +import org.bouncycastle.asn1.x509.AlgorithmIdentifier +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory +import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder +import org.bouncycastle.util.BigIntegers import java.io.IOException import java.nio.file.Files import java.nio.file.Path +import java.security.KeyFactory +import java.security.KeyStore +import java.time.OffsetDateTime +import java.time.Period +import java.time.ZoneOffset +import java.util.Date class Pkcs12KeyStore private constructor(private val path: Path) { fun signJar(jar: Path) { @@ -10,8 +26,8 @@ class Pkcs12KeyStore private constructor(private val path: Path) { "jarsigner", "-keystore", path.toString(), "-storetype", "pkcs12", - "-storepass", STORE_PASSWORD, - "-keypass", KEY_PASSWORD, + "-storepass", PASSWORD, + "-keypass", PASSWORD, jar.toString(), ALIAS ) @@ -19,36 +35,68 @@ class Pkcs12KeyStore private constructor(private val path: Path) { companion object { private const val ALIAS = "openrs2" - private const val STORE_PASSWORD = ALIAS - private const val KEY_PASSWORD = ALIAS - private const val DNAME = "CN=OpenRS2" - private const val VALIDITY_PERIOD = "3650" - private const val KEY_ALGORITHM = "RSA" - private const val KEY_SIZE = "2048" - private const val SIGNATURE_ALGORITHM = "SHA256with$KEY_ALGORITHM" + + private const val PASSWORD = ALIAS + private val PASSWORD_CHARS = PASSWORD.toCharArray() + private val PASSWORD_PARAMETER = KeyStore.PasswordProtection(PASSWORD_CHARS) + + private const val SERIAL_LENGTH = 128 + + // TODO(gpe): add support for overriding this + private val DNAME = X500NameBuilder() + .addRDN(BCStyle.CN, "OpenRS2") + .build() + + private val MAX_CLOCK_SKEW = Period.ofDays(1) + private val VALIDITY_PERIOD = Period.ofYears(1) + + private val SHA256_WITH_RSA = AlgorithmIdentifier(PKCSObjectIdentifiers.sha256WithRSAEncryption) + private val SHA256 = AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256) fun open(path: Path): Pkcs12KeyStore { - if (!Files.exists(path)) { - create(path) + val keyStore = KeyStore.getInstance("PKCS12") + if (Files.exists(path)) { + Files.newInputStream(path).use { input -> + keyStore.load(input, PASSWORD_CHARS) + } + } else { + keyStore.load(null) + } + + if (!keyStore.containsAlias(ALIAS)) { + keyStore.setEntry(ALIAS, createPrivateKeyEntry(), PASSWORD_PARAMETER) + + Files.newOutputStream(path).use { output -> + keyStore.store(output, PASSWORD_CHARS) + } } + return Pkcs12KeyStore(path) } - private fun create(path: Path) { - exec( - "keytool", - "-genkeypair", - "-keystore", path.toString(), - "-storetype", "pkcs12", - "-storepass", STORE_PASSWORD, - "-keypass", KEY_PASSWORD, - "-alias", ALIAS, - "-dname", DNAME, - "-validity", VALIDITY_PERIOD, - "-keyalg", KEY_ALGORITHM, - "-keysize", KEY_SIZE, - "-sigalg", SIGNATURE_ALGORITHM - ) + private fun createPrivateKeyEntry(): KeyStore.PrivateKeyEntry { + val (public, private) = Rsa.generateKeyPair(Rsa.JAR_KEY_LENGTH) + + val serial = BigIntegers.createRandomBigInteger(SERIAL_LENGTH, secureRandom) + + val start = OffsetDateTime.now(ZoneOffset.UTC).minus(MAX_CLOCK_SKEW) + val end = start.plus(VALIDITY_PERIOD) + + val spki = SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(public) + val signer = BcRSAContentSignerBuilder(SHA256_WITH_RSA, SHA256).build(private) + + val certificate = X509v3CertificateBuilder( + DNAME, + serial, + Date.from(start.toInstant()), + Date.from(end.toInstant()), + DNAME, + spki + ).build(signer) + + val jcaPrivate = KeyFactory.getInstance("RSA").generatePrivate(private.toKeySpec()) + val jcaCertificate = JcaX509CertificateConverter().getCertificate(certificate) + return KeyStore.PrivateKeyEntry(jcaPrivate, arrayOf(jcaCertificate)) } private fun exec(command: String, vararg args: String) { diff --git a/common/src/main/java/dev/openrs2/common/crypto/Rsa.kt b/common/src/main/java/dev/openrs2/common/crypto/Rsa.kt index 7ef022cb..67835756 100644 --- a/common/src/main/java/dev/openrs2/common/crypto/Rsa.kt +++ b/common/src/main/java/dev/openrs2/common/crypto/Rsa.kt @@ -24,6 +24,8 @@ import java.io.IOException import java.math.BigInteger import java.nio.file.Files import java.nio.file.Path +import java.security.spec.KeySpec +import java.security.spec.RSAPrivateCrtKeySpec val RSAPrivateCrtKeyParameters.publicKey get() = RSAKeyParameters(false, modulus, publicExponent) @@ -45,6 +47,10 @@ fun ByteBuf.rsaDecrypt(key: RSAKeyParameters): ByteBuf { return Rsa.decrypt(toBigInteger(), key).toByteBuf() } +fun RSAPrivateCrtKeyParameters.toKeySpec(): KeySpec { + return RSAPrivateCrtKeySpec(modulus, publicExponent, exponent, p, q, dp, dq, qInv) +} + object Rsa { private const val PUBLIC_KEY = "PUBLIC KEY" private const val PRIVATE_KEY = "PRIVATE KEY" @@ -61,6 +67,8 @@ object Rsa { */ const val CLIENT_KEY_LENGTH = 1008 + const val JAR_KEY_LENGTH = 2048 + // 1 in 2^80 private const val CERTAINTY = 80