Add support for signed master indexes

This commit also changes the way the master index format detection
works, as the previous scheme could not distinguish VERSIONED from
WHIRLPOOL.

Signed-off-by: Graham <gpe@openrs2.org>
Graham 4 years ago
parent ed7eb10411
commit 5d7bd5b5c7
  1. 7
      archive/src/main/kotlin/org/openrs2/archive/cache/CacheImporter.kt
  2. 5
      archive/src/main/kotlin/org/openrs2/archive/cache/ImportCommand.kt
  3. 188
      cache/src/main/kotlin/org/openrs2/cache/Js5MasterIndex.kt
  4. 3
      cache/src/main/kotlin/org/openrs2/cache/MasterIndexFormat.kt
  5. 259
      cache/src/test/kotlin/org/openrs2/cache/Js5MasterIndexTest.kt
  6. 0
      cache/src/test/resources/org/openrs2/cache/master-index/original/255/0.dat
  7. 28
      cache/src/test/resources/org/openrs2/cache/master-index/private.key
  8. 9
      cache/src/test/resources/org/openrs2/cache/master-index/public.key
  9. BIN
      cache/src/test/resources/org/openrs2/cache/master-index/versioned/255/0.dat
  10. 0
      cache/src/test/resources/org/openrs2/cache/master-index/versioned/255/1.dat
  11. 0
      cache/src/test/resources/org/openrs2/cache/master-index/versioned/255/3.dat
  12. 0
      cache/src/test/resources/org/openrs2/cache/master-index/versioned/255/6.dat
  13. BIN
      cache/src/test/resources/org/openrs2/cache/master-index/whirlpool/255/0.dat
  14. BIN
      cache/src/test/resources/org/openrs2/cache/master-index/whirlpool/255/1.dat

@ -64,7 +64,6 @@ public class CacheImporter @Inject constructor(
public suspend fun import(
store: Store,
masterIndexFormat: MasterIndexFormat?,
game: String,
build: Int?,
timestamp: Instant?,
@ -77,7 +76,7 @@ public class CacheImporter @Inject constructor(
val gameId = getGameId(connection, game)
// import master index
val masterIndex = createMasterIndex(store, masterIndexFormat)
val masterIndex = createMasterIndex(store)
try {
addMasterIndex(connection, masterIndex, gameId, build, timestamp, name, description, false)
} finally {
@ -316,11 +315,11 @@ public class CacheImporter @Inject constructor(
}
}
private fun createMasterIndex(store: Store, format: MasterIndexFormat?): MasterIndex {
private fun createMasterIndex(store: Store): MasterIndex {
val index = Js5MasterIndex.create(store)
alloc.buffer().use { uncompressed ->
index.write(uncompressed, format ?: index.minimumFormat)
index.write(uncompressed)
Js5Compression.compress(uncompressed, Js5CompressionType.UNCOMPRESSED).use { buf ->
return MasterIndex(index, buf.retain())

@ -3,18 +3,15 @@ package org.openrs2.archive.cache
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.int
import com.github.ajalt.clikt.parameters.types.path
import com.google.inject.Guice
import kotlinx.coroutines.runBlocking
import org.openrs2.archive.ArchiveModule
import org.openrs2.cache.MasterIndexFormat
import org.openrs2.cache.Store
import org.openrs2.cli.instant
public class ImportCommand : CliktCommand(name = "import") {
private val masterIndexFormat by option().enum<MasterIndexFormat>()
private val build by option().int()
private val timestamp by option().instant()
private val name by option()
@ -32,7 +29,7 @@ public class ImportCommand : CliktCommand(name = "import") {
val importer = injector.getInstance(CacheImporter::class.java)
Store.open(input).use { store ->
importer.import(store, masterIndexFormat, game, build, timestamp, name, description)
importer.import(store, game, build, timestamp, name, description)
}
}
}

@ -1,36 +1,113 @@
package org.openrs2.cache
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufUtil
import org.bouncycastle.crypto.params.RSAKeyParameters
import org.openrs2.buffer.crc32
import org.openrs2.buffer.use
import org.openrs2.crypto.Rsa
import org.openrs2.crypto.Whirlpool
import org.openrs2.crypto.rsaDecrypt
import org.openrs2.crypto.rsaEncrypt
import org.openrs2.crypto.whirlpool
public inline class Js5MasterIndex(public val entries: MutableList<Entry> = mutableListOf()) {
public data class Entry(public var version: Int, public var checksum: Int)
public data class Js5MasterIndex(
public var format: MasterIndexFormat,
public val entries: MutableList<Entry> = mutableListOf()
) {
public class Entry(
public var version: Int,
public var checksum: Int,
digest: ByteArray?
) {
public var digest: ByteArray? = digest
set(value) {
require(value == null || value.size == Whirlpool.DIGESTBYTES)
field = value
}
public val minimumFormat: MasterIndexFormat
get() {
for (entry in entries) {
if (entry.version != 0) {
return MasterIndexFormat.VERSIONED
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Entry
if (version != other.version) return false
if (checksum != other.checksum) return false
if (digest != null) {
if (other.digest == null) return false
if (!digest.contentEquals(other.digest)) return false
} else if (other.digest != null) return false
return true
}
override fun hashCode(): Int {
var result = version
result = 31 * result + checksum
result = 31 * result + (digest?.contentHashCode() ?: 0)
return result
}
override fun toString(): String {
val digest = digest
val hex = if (digest != null) {
ByteBufUtil.hexDump(digest)
} else {
"null"
}
return "Entry(version=$version, checksum=$checksum, digest=$hex)"
}
}
public fun write(buf: ByteBuf, key: RSAKeyParameters? = null) {
val start = buf.writerIndex()
return MasterIndexFormat.ORIGINAL
if (format >= MasterIndexFormat.WHIRLPOOL) {
buf.writeByte(entries.size)
}
public fun write(buf: ByteBuf, format: MasterIndexFormat) {
for (entry in entries) {
buf.writeInt(entry.checksum)
if (format >= MasterIndexFormat.VERSIONED) {
buf.writeInt(entry.version)
}
if (format >= MasterIndexFormat.WHIRLPOOL) {
val digest = entry.digest
if (digest != null) {
buf.writeBytes(digest)
} else {
buf.writeZero(Whirlpool.DIGESTBYTES)
}
}
}
if (format >= MasterIndexFormat.WHIRLPOOL) {
val digest = buf.whirlpool(start, buf.writerIndex() - start)
if (key != null) {
buf.alloc().buffer(SIGNATURE_LENGTH, SIGNATURE_LENGTH).use { plaintext ->
plaintext.writeByte(Rsa.MAGIC)
plaintext.writeBytes(digest)
plaintext.rsaEncrypt(key).use { ciphertext ->
buf.writeBytes(ciphertext)
}
}
} else {
buf.writeByte(Rsa.MAGIC)
buf.writeBytes(digest)
}
}
}
public companion object {
private const val SIGNATURE_LENGTH = Whirlpool.DIGESTBYTES + 1
public fun create(store: Store): Js5MasterIndex {
val index = Js5MasterIndex()
val masterIndex = Js5MasterIndex(MasterIndexFormat.ORIGINAL)
var nextArchive = 0
for (archive in store.list(Js5Archive.ARCHIVESET)) {
@ -40,46 +117,111 @@ public inline class Js5MasterIndex(public val entries: MutableList<Entry> = muta
* entries with a zero CRC are probably invalid.
*/
for (i in nextArchive until archive) {
index.entries += Entry(0, 0)
masterIndex.entries += Entry(0, 0, null)
}
val entry = store.read(Js5Archive.ARCHIVESET, archive).use { buf ->
val checksum = buf.crc32()
val digest = buf.whirlpool()
val version = Js5Compression.uncompress(buf).use { uncompressed ->
Js5Index.read(uncompressed).version
val index = Js5Index.read(uncompressed)
if (index.hasDigests) {
masterIndex.format = maxOf(masterIndex.format, MasterIndexFormat.WHIRLPOOL)
} else if (index.protocol >= Js5Protocol.VERSIONED) {
masterIndex.format = maxOf(masterIndex.format, MasterIndexFormat.VERSIONED)
}
index.version
}
// TODO(gpe): should we throw an exception if there are trailing bytes here or in the block above?
Entry(version, checksum)
Entry(version, checksum, digest)
}
index.entries += entry
masterIndex.entries += entry
nextArchive = archive + 1
}
return index
return masterIndex
}
public fun read(buf: ByteBuf, format: MasterIndexFormat): Js5MasterIndex {
when (format) {
public fun read(buf: ByteBuf, format: MasterIndexFormat, key: RSAKeyParameters? = null): Js5MasterIndex {
val index = Js5MasterIndex(format)
val start = buf.readerIndex()
val len = buf.readableBytes()
val archives = when (format) {
MasterIndexFormat.ORIGINAL -> {
require(buf.readableBytes() % 4 == 0)
require(len % 4 == 0) {
"Length is not a multiple of 4 bytes"
}
len / 4
}
MasterIndexFormat.VERSIONED -> {
require(buf.readableBytes() % 8 == 0)
require(len % 8 == 0) {
"Length is not a multiple of 8 bytes"
}
len / 8
}
MasterIndexFormat.WHIRLPOOL -> {
buf.readUnsignedByte().toInt()
}
}
val index = Js5MasterIndex()
while (buf.isReadable) {
for (i in 0 until archives) {
val checksum = buf.readInt()
val version = if (format >= MasterIndexFormat.VERSIONED) {
buf.readInt()
} else {
0
}
index.entries += Entry(version, checksum)
val digest = if (format >= MasterIndexFormat.WHIRLPOOL) {
val bytes = ByteArray(Whirlpool.DIGESTBYTES)
buf.readBytes(bytes)
bytes
} else {
null
}
index.entries += Entry(version, checksum, digest)
}
val end = buf.readerIndex()
if (format >= MasterIndexFormat.WHIRLPOOL) {
val ciphertext = buf.readSlice(buf.readableBytes())
decrypt(ciphertext, key).use { plaintext ->
require(plaintext.readableBytes() == SIGNATURE_LENGTH) {
"Invalid signature length"
}
// the client doesn't verify what I presume is the RSA magic byte
plaintext.skipBytes(1)
val expected = ByteArray(Whirlpool.DIGESTBYTES)
plaintext.readBytes(expected)
val actual = buf.whirlpool(start, end - start)
require(expected.contentEquals(actual)) {
"Invalid signature"
}
}
}
return index
}
private fun decrypt(buf: ByteBuf, key: RSAKeyParameters?): ByteBuf {
return if (key != null) {
buf.rsaDecrypt(key)
} else {
buf.retain()
}
}
}
}

@ -2,5 +2,6 @@ package org.openrs2.cache
public enum class MasterIndexFormat {
ORIGINAL,
VERSIONED
VERSIONED,
WHIRLPOOL
}

@ -1,8 +1,10 @@
package org.openrs2.cache
import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.ByteBufUtil
import io.netty.buffer.Unpooled
import org.openrs2.buffer.use
import org.openrs2.crypto.Rsa
import java.nio.file.Path
import kotlin.test.Test
import kotlin.test.assertEquals
@ -10,47 +12,169 @@ import kotlin.test.assertFailsWith
object Js5MasterIndexTest {
private val ROOT = Path.of(FlatFileStoreTest::class.java.getResource("master-index").toURI())
private val PRIVATE_KEY = Rsa.readPrivateKey(ROOT.resolve("private.key"))
private val PUBLIC_KEY = Rsa.readPublicKey(ROOT.resolve("public.key"))
private val encodedOriginal = byteArrayOf(0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 5)
private val encodedOriginal = ByteBufUtil.decodeHexDump("000000010000000300000005")
private val decodedOriginal = Js5MasterIndex(
MasterIndexFormat.ORIGINAL,
mutableListOf(
Js5MasterIndex.Entry(0, 1),
Js5MasterIndex.Entry(0, 3),
Js5MasterIndex.Entry(0, 5)
Js5MasterIndex.Entry(0, 1, null),
Js5MasterIndex.Entry(0, 3, null),
Js5MasterIndex.Entry(0, 5, null)
)
)
private val encodedVersioned = byteArrayOf(0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0, 4)
private val encodedVersioned = ByteBufUtil.decodeHexDump("000000010000000000000003000000020000000500000004")
private val decodedVersioned = Js5MasterIndex(
MasterIndexFormat.VERSIONED,
mutableListOf(
Js5MasterIndex.Entry(0, 1),
Js5MasterIndex.Entry(2, 3),
Js5MasterIndex.Entry(4, 5)
Js5MasterIndex.Entry(0, 1, null),
Js5MasterIndex.Entry(2, 3, null),
Js5MasterIndex.Entry(4, 5, null)
)
)
private val encodedWhirlpool = ByteBufUtil.decodeHexDump(
"01" +
"89abcdef" +
"01234567" +
"0e1a2b93c80a41c7ad2a985dff707a6a8ff82e229cbc468f04191198920955a1" +
"4b3d7eab77a17faf99208dee5b44afb789962ad79f230b3b59106a0af892219c" +
"0a" +
"ee8f66a2ce0b07de4d2b792eed26ae7a6c307b763891d085c63ea55b4c003bc0" +
"b3ecb77cc1a8f9ccd53c405b3264e598820b4940f630ff079a9feb950f639671"
)
private val decodedWhirlpool = Js5MasterIndex(
MasterIndexFormat.WHIRLPOOL,
mutableListOf(
Js5MasterIndex.Entry(
0x01234567, 0x89ABCDEF.toInt(), ByteBufUtil.decodeHexDump(
"0e1a2b93c80a41c7ad2a985dff707a6a8ff82e229cbc468f04191198920955a1" +
"4b3d7eab77a17faf99208dee5b44afb789962ad79f230b3b59106a0af892219c"
)
)
)
)
private val encodedWhirlpoolNullDigest = ByteBufUtil.decodeHexDump(
"01" +
"89abcdef" +
"01234567" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"0a" +
"4a0e22540fb0a9bc06fe84bfb35f9281ba9fbd30288c3375c508ad741c4d4491" +
"8a65765bc2dce9d67029be79bd544f96055a41d725c080bc5b85a48b5aae6e4d"
)
private val decodedWhirlpoolNullDigest = Js5MasterIndex(
MasterIndexFormat.WHIRLPOOL,
mutableListOf(
Js5MasterIndex.Entry(0x01234567, 0x89ABCDEF.toInt(), null)
)
)
private val encodedSigned = ByteBufUtil.decodeHexDump(
"01" +
"89abcdef" +
"01234567" +
"0e1a2b93c80a41c7ad2a985dff707a6a8ff82e229cbc468f04191198920955a1" +
"4b3d7eab77a17faf99208dee5b44afb789962ad79f230b3b59106a0af892219c" +
"2134b1e637d4c9f3b7bdd446ad40cedb6d824cfb48f937ae0d6e2ba3977881ea" +
"ed02adae179ed89cea56e98772186bb569bb24a4951e441716df0d5d7199c088" +
"28974d43c3644e74bf29ec1435e425f6cb05aca14a84163c5b46b6e6a9362f22" +
"4f69f4a5888b3fe7aec0141da25b17c7f65069eed59f3be134fa1ade4e191b41" +
"d561447446cd1cc4d11e6499c49e00066173908491d8d2ff282aefa86e6c6b15" +
"dceb437d0436b6195ef60d4128e1e0184bf6929b73abd1a8aa2a047e3cb90d03" +
"57707ce3f4f5a7af8471eda5c0c0748454a9cbb48c25ebe4e7fd94e3881b6461" +
"d06e2bce128dc96decb537b8e9611591d445d7dfd3701d25ac05f8d091581aef"
)
@Test
fun testCreateOriginal() {
val index = Store.open(ROOT.resolve("original")).use { store ->
Js5MasterIndex.create(store)
}
assertEquals(
Js5MasterIndex(
MasterIndexFormat.ORIGINAL,
mutableListOf(
Js5MasterIndex.Entry(
0, 609698396, ByteBufUtil.decodeHexDump(
"0e1a2b93c80a41c7ad2a985dff707a6a8ff82e229cbc468f04191198920955a1" +
"4b3d7eab77a17faf99208dee5b44afb789962ad79f230b3b59106a0af892219c"
)
),
)
), index
)
}
@Test
fun testMinimumFormat() {
assertEquals(MasterIndexFormat.ORIGINAL, decodedOriginal.minimumFormat)
assertEquals(MasterIndexFormat.VERSIONED, decodedVersioned.minimumFormat)
fun testCreateVersioned() {
val index = Store.open(ROOT.resolve("versioned")).use { store ->
Js5MasterIndex.create(store)
}
assertEquals(
Js5MasterIndex(
MasterIndexFormat.VERSIONED,
mutableListOf(
Js5MasterIndex.Entry(
0, 609698396, ByteBufUtil.decodeHexDump(
"0e1a2b93c80a41c7ad2a985dff707a6a8ff82e229cbc468f04191198920955a1" +
"4b3d7eab77a17faf99208dee5b44afb789962ad79f230b3b59106a0af892219c"
)
),
Js5MasterIndex.Entry(
0x12345678, 78747481, ByteBufUtil.decodeHexDump(
"180ff4ad371f56d4a90d81e0b69b23836cd9b101b828f18b7e6d232c4d302539" +
"638eb2e9259957645aae294f09b2d669c93dbbfc0d8359f1b232ae468f678ca1"
)
),
Js5MasterIndex.Entry(0, 0, null),
Js5MasterIndex.Entry(
0x9ABCDEF0.toInt(), -456081154, ByteBufUtil.decodeHexDump(
"972003261b7628525346e0052567662e5695147ad710f877b63b9ab53b3f6650" +
"ca003035fde4398b2ef73a60e4b13798aa597a30c1bf0a13c0cd412394af5f96"
)
),
Js5MasterIndex.Entry(0, 0, null),
Js5MasterIndex.Entry(0, 0, null),
Js5MasterIndex.Entry(
0xAA55AA55.toInt(), 186613982, ByteBufUtil.decodeHexDump(
"d50a6e9abd3b5269606304dc2769cbc8618e1ae6ff705291c0dfcc374e450dd2" +
"5f1be5f1d5459651d22d3e87ef0a1c69be7807f661cd001be24a6609f6d57916"
)
)
)
), index
)
}
@Test
fun testCreate() {
val index = Store.open(ROOT).use { store ->
fun testCreateWhirlpool() {
val index = Store.open(ROOT.resolve("whirlpool")).use { store ->
Js5MasterIndex.create(store)
}
assertEquals(
Js5MasterIndex(
MasterIndexFormat.WHIRLPOOL,
mutableListOf(
Js5MasterIndex.Entry(0, 609698396),
Js5MasterIndex.Entry(0x12345678, 78747481),
Js5MasterIndex.Entry(0, 0),
Js5MasterIndex.Entry(0x9ABCDEF0.toInt(), -456081154),
Js5MasterIndex.Entry(0, 0),
Js5MasterIndex.Entry(0, 0),
Js5MasterIndex.Entry(0xAA55AA55.toInt(), 186613982)
Js5MasterIndex.Entry(
0, 668177970, ByteBufUtil.decodeHexDump(
"2faa83116e1d1719d5db15f128eb57f62afbf0207c47bced3f558ec17645d138" +
"72f4fb9b0e36a5f6f5d30e1295b3fa49556dfd0819cb5137f3b69f64155f3fb7"
)
),
Js5MasterIndex.Entry(
0, 1925442845, ByteBufUtil.decodeHexDump(
"fcc45b0ab6d0067889e44de0004bcbb6cc538aff8f80edf1b49b583cedd73fea" +
"937ae6990235257fe8aa35c44d35450c13e670711337ee5116957cd98cc27985"
)
)
)
), index
)
@ -73,7 +197,7 @@ object Js5MasterIndexTest {
@Test
fun testWriteOriginal() {
ByteBufAllocator.DEFAULT.buffer().use { actual ->
decodedOriginal.write(actual, MasterIndexFormat.ORIGINAL)
decodedOriginal.write(actual)
Unpooled.wrappedBuffer(encodedOriginal).use { expected ->
assertEquals(expected, actual)
@ -98,11 +222,102 @@ object Js5MasterIndexTest {
@Test
fun testWriteVersioned() {
ByteBufAllocator.DEFAULT.buffer().use { actual ->
decodedVersioned.write(actual, MasterIndexFormat.VERSIONED)
decodedVersioned.write(actual)
Unpooled.wrappedBuffer(encodedVersioned).use { expected ->
assertEquals(expected, actual)
}
}
}
@Test
fun testReadWhirlpool() {
Unpooled.wrappedBuffer(encodedWhirlpool).use { buf ->
val index = Js5MasterIndex.read(buf, MasterIndexFormat.WHIRLPOOL)
assertEquals(decodedWhirlpool, index)
}
}
@Test
fun testReadWhirlpoolInvalidSignature() {
Unpooled.copiedBuffer(encodedWhirlpool).use { buf ->
val lastIndex = buf.writerIndex() - 1
buf.setByte(lastIndex, buf.getByte(lastIndex).toInt().inv())
assertFailsWith<IllegalArgumentException> {
Js5MasterIndex.read(buf, MasterIndexFormat.WHIRLPOOL)
}
}
}
@Test
fun testReadWhirlpoolInvalidSignatureLength() {
Unpooled.wrappedBuffer(encodedWhirlpool, 0, encodedWhirlpool.size - 1).use { buf ->
assertFailsWith<IllegalArgumentException> {
Js5MasterIndex.read(buf, MasterIndexFormat.WHIRLPOOL)
}
}
}
@Test
fun testWriteWhirlpool() {
ByteBufAllocator.DEFAULT.buffer().use { actual ->
decodedWhirlpool.write(actual)
Unpooled.wrappedBuffer(encodedWhirlpool).use { expected ->
assertEquals(expected, actual)
}
}
}
@Test
fun testWriteWhirlpoolNullDigest() {
ByteBufAllocator.DEFAULT.buffer().use { actual ->
decodedWhirlpoolNullDigest.write(actual)
Unpooled.wrappedBuffer(encodedWhirlpoolNullDigest).use { expected ->
assertEquals(expected, actual)
}
}
}
@Test
fun testReadSigned() {
Unpooled.wrappedBuffer(encodedSigned).use { buf ->
val index = Js5MasterIndex.read(buf, MasterIndexFormat.WHIRLPOOL, PUBLIC_KEY)
assertEquals(decodedWhirlpool, index)
}
}
@Test
fun testReadSignedInvalidSignature() {
Unpooled.copiedBuffer(encodedSigned).use { buf ->
val lastIndex = buf.writerIndex() - 1
buf.setByte(lastIndex, buf.getByte(lastIndex).toInt().inv())
assertFailsWith<IllegalArgumentException> {
Js5MasterIndex.read(buf, MasterIndexFormat.WHIRLPOOL, PUBLIC_KEY)
}
}
}
@Test
fun testReadSignedInvalidSignatureLength() {
Unpooled.wrappedBuffer(encodedSigned, 0, encodedSigned.size - 1).use { buf ->
assertFailsWith<IllegalArgumentException> {
Js5MasterIndex.read(buf, MasterIndexFormat.WHIRLPOOL, PUBLIC_KEY)
}
}
}
@Test
fun testWriteSigned() {
ByteBufAllocator.DEFAULT.buffer().use { actual ->
decodedWhirlpool.write(actual, PRIVATE_KEY)
Unpooled.wrappedBuffer(encodedSigned).use { expected ->
assertEquals(expected, actual)
}
}
}
}

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDErqLXr8aHZeAz
5ajLpsICaDGue4fOWntCF+52Rws8vHyxkuPGtMXAdXCDFr0LCYPGykcqsHMqF2/+
KNtPindRpQ6spJEDbGhLROSj7Pybc8A2livS3GG5LbZjTbkzIqpUYcR7p4VrbxEO
IN5Y7B9vzMpqHhcMCfkzn9k6RypzwMYCqNZUNJVJotJTALSg4gG2GEmZeqyUy7oJ
cFew+L6xnrEsZ41S0ouWoE0wCWuxikJeMD6eFZRNjRMt8mV4NB28y2iNUuJbI+Yh
Kg6kI5bflnF14sUY/yr996coXWEc6hzyCjHU30ItmtD6f9W48pXm7q3jhKhBvdsw
T8ofLS9JAgMBAAECggEAN/nr5q7kNczMznhiXfOL69tzqFEICba+tcTR3V/C1vpe
ErvXj8oLLgc+ilCCZQ3EL2OKdZ/aZvRcr105bZ3e76GmV4ROsUa9OA7Xc8AS9Lsw
zVhYCJ8oQOe9rO2F9AO9rl5U6Ux4MGdf10GLhotNNyh1w1XlAUwlXIf17xwp/N80
H2vowW3U1cb1AMaJsNGtpSolGEIb6jA7S0CjNLf2eNXeNjswpDyGgFRp5bFr6N6s
151kGOZSt+qpei9I91lh8zMPYVhSrz42wjH8HgisHKH8VaWLiQTdaqhqNU5nJZ1W
UGejOCPcMeeOTrnmM24tIs9Xeh/bWnYRwDodM5RAcQKBgQD35/B1ehlbHxMNKYxK
uaucqfjQ8aDJU2tB8rr/VfjGWzpB2mcOj7/gHCEuSXQTkdQ2SXlGU92w5jLkav3g
vSERk3Vnh/t/Dq9HysqeuzAp8SXavnDvYGLTCUpMyFECXrPIKrMs+U2F4pTOOXbF
UBQQ1f8WRwes0bd0ahIdyJW9awKBgQDLGo4aSWibBAZC/z4ja4fjR2sJuWAPxoIM
N3wa9VYCibgrvJlaXvlEsYWDcvkFaaBWjJYWrFjHX7PCwtW7J9rBw3tpGzIcNXCj
YfJRjMMxkPiCMF07JEPBiRo1WUmF1O8ubzK8LDr9DRlxSrjzrbWAYtcvY/M3k5Sz
OA0RHKbfGwKBgHYxoRWBh0FIiX7HBlpCN63T5AtKiIw0N3kTz1AZnyiDKj1ncach
piulfbRh1PPXnUPct/Nt3M6QkkcRM8XIplGI6nrX/HJRgARMjVosiQQWMyQdlB6s
57ESRthg0S6+FB0lLpQMsIdaxfOkthnQ2iBExv/KEcC1pC/eupB0p9/NAoGBAJum
mTq6AWmzVt0nYUah1P0wMW69W0obtnSIXRsH48eEJdmW6uugF2Y2qfyIMyGbxl4t
1aRAprT8ufXLfSK2M2cFWeG+DtQhfFYp7RvkRX8J+/lB+WEmtKpwWN6Ds93VxwuN
+pLNTtO5o0L4oe9Vs+BVX1YZQj7YYkBK93CixZv7AoGBAL45w+WCiN4lSLfivfcO
fk82aWISwAMxDFCRbNQ2/T8xSlP+9Y8x5ixseK0tBRyKvqzay4jinpDjtslKQCyb
XJ/pvrxlTQDOD7uK+IpfEpSrZPHpGRnosx2QUsdDbDr0KBupQvx5i2BGVfatNDM1
K3Z5TSOop7aSYoEjkH8O+mdl
-----END PRIVATE KEY-----

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxK6i16/Gh2XgM+Woy6bC
AmgxrnuHzlp7QhfudkcLPLx8sZLjxrTFwHVwgxa9CwmDxspHKrBzKhdv/ijbT4p3
UaUOrKSRA2xoS0Tko+z8m3PANpYr0txhuS22Y025MyKqVGHEe6eFa28RDiDeWOwf
b8zKah4XDAn5M5/ZOkcqc8DGAqjWVDSVSaLSUwC0oOIBthhJmXqslMu6CXBXsPi+
sZ6xLGeNUtKLlqBNMAlrsYpCXjA+nhWUTY0TLfJleDQdvMtojVLiWyPmISoOpCOW
35ZxdeLFGP8q/fenKF1hHOoc8gox1N9CLZrQ+n/VuPKV5u6t44SoQb3bME/KHy0v
SQIDAQAB
-----END PUBLIC KEY-----
Loading…
Cancel
Save