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. 186
      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( public suspend fun import(
store: Store, store: Store,
masterIndexFormat: MasterIndexFormat?,
game: String, game: String,
build: Int?, build: Int?,
timestamp: Instant?, timestamp: Instant?,
@ -77,7 +76,7 @@ public class CacheImporter @Inject constructor(
val gameId = getGameId(connection, game) val gameId = getGameId(connection, game)
// import master index // import master index
val masterIndex = createMasterIndex(store, masterIndexFormat) val masterIndex = createMasterIndex(store)
try { try {
addMasterIndex(connection, masterIndex, gameId, build, timestamp, name, description, false) addMasterIndex(connection, masterIndex, gameId, build, timestamp, name, description, false)
} finally { } 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) val index = Js5MasterIndex.create(store)
alloc.buffer().use { uncompressed -> alloc.buffer().use { uncompressed ->
index.write(uncompressed, format ?: index.minimumFormat) index.write(uncompressed)
Js5Compression.compress(uncompressed, Js5CompressionType.UNCOMPRESSED).use { buf -> Js5Compression.compress(uncompressed, Js5CompressionType.UNCOMPRESSED).use { buf ->
return MasterIndex(index, buf.retain()) 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.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.option 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.int
import com.github.ajalt.clikt.parameters.types.path import com.github.ajalt.clikt.parameters.types.path
import com.google.inject.Guice import com.google.inject.Guice
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.openrs2.archive.ArchiveModule import org.openrs2.archive.ArchiveModule
import org.openrs2.cache.MasterIndexFormat
import org.openrs2.cache.Store import org.openrs2.cache.Store
import org.openrs2.cli.instant import org.openrs2.cli.instant
public class ImportCommand : CliktCommand(name = "import") { public class ImportCommand : CliktCommand(name = "import") {
private val masterIndexFormat by option().enum<MasterIndexFormat>()
private val build by option().int() private val build by option().int()
private val timestamp by option().instant() private val timestamp by option().instant()
private val name by option() private val name by option()
@ -32,7 +29,7 @@ public class ImportCommand : CliktCommand(name = "import") {
val importer = injector.getInstance(CacheImporter::class.java) val importer = injector.getInstance(CacheImporter::class.java)
Store.open(input).use { store -> 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 package org.openrs2.cache
import io.netty.buffer.ByteBuf 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.crc32
import org.openrs2.buffer.use 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 Js5MasterIndex(
public data class Entry(public var version: Int, public var checksum: Int) 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 override fun equals(other: Any?): Boolean {
get() { if (this === other) return true
for (entry in entries) { if (javaClass != other?.javaClass) return false
if (entry.version != 0) {
return MasterIndexFormat.VERSIONED 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)"
} }
} }
return MasterIndexFormat.ORIGINAL public fun write(buf: ByteBuf, key: RSAKeyParameters? = null) {
val start = buf.writerIndex()
if (format >= MasterIndexFormat.WHIRLPOOL) {
buf.writeByte(entries.size)
} }
public fun write(buf: ByteBuf, format: MasterIndexFormat) {
for (entry in entries) { for (entry in entries) {
buf.writeInt(entry.checksum) buf.writeInt(entry.checksum)
if (format >= MasterIndexFormat.VERSIONED) { if (format >= MasterIndexFormat.VERSIONED) {
buf.writeInt(entry.version) 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 { public companion object {
private const val SIGNATURE_LENGTH = Whirlpool.DIGESTBYTES + 1
public fun create(store: Store): Js5MasterIndex { public fun create(store: Store): Js5MasterIndex {
val index = Js5MasterIndex() val masterIndex = Js5MasterIndex(MasterIndexFormat.ORIGINAL)
var nextArchive = 0 var nextArchive = 0
for (archive in store.list(Js5Archive.ARCHIVESET)) { 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. * entries with a zero CRC are probably invalid.
*/ */
for (i in nextArchive until archive) { 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 entry = store.read(Js5Archive.ARCHIVESET, archive).use { buf ->
val checksum = buf.crc32() val checksum = buf.crc32()
val digest = buf.whirlpool()
val version = Js5Compression.uncompress(buf).use { uncompressed -> 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? // 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 nextArchive = archive + 1
} }
return index return masterIndex
} }
public fun read(buf: ByteBuf, format: MasterIndexFormat): Js5MasterIndex { public fun read(buf: ByteBuf, format: MasterIndexFormat, key: RSAKeyParameters? = null): Js5MasterIndex {
when (format) { val index = Js5MasterIndex(format)
val start = buf.readerIndex()
val len = buf.readableBytes()
val archives = when (format) {
MasterIndexFormat.ORIGINAL -> { MasterIndexFormat.ORIGINAL -> {
require(buf.readableBytes() % 4 == 0) require(len % 4 == 0) {
"Length is not a multiple of 4 bytes"
}
len / 4
} }
MasterIndexFormat.VERSIONED -> { 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() for (i in 0 until archives) {
while (buf.isReadable) {
val checksum = buf.readInt() val checksum = buf.readInt()
val version = if (format >= MasterIndexFormat.VERSIONED) { val version = if (format >= MasterIndexFormat.VERSIONED) {
buf.readInt() buf.readInt()
} else { } else {
0 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 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 { public enum class MasterIndexFormat {
ORIGINAL, ORIGINAL,
VERSIONED VERSIONED,
WHIRLPOOL
} }

@ -1,8 +1,10 @@
package org.openrs2.cache package org.openrs2.cache
import io.netty.buffer.ByteBufAllocator import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.ByteBufUtil
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import org.openrs2.buffer.use import org.openrs2.buffer.use
import org.openrs2.crypto.Rsa
import java.nio.file.Path import java.nio.file.Path
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -10,47 +12,169 @@ import kotlin.test.assertFailsWith
object Js5MasterIndexTest { object Js5MasterIndexTest {
private val ROOT = Path.of(FlatFileStoreTest::class.java.getResource("master-index").toURI()) 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( private val decodedOriginal = Js5MasterIndex(
MasterIndexFormat.ORIGINAL,
mutableListOf( mutableListOf(
Js5MasterIndex.Entry(0, 1), Js5MasterIndex.Entry(0, 1, null),
Js5MasterIndex.Entry(0, 3), Js5MasterIndex.Entry(0, 3, null),
Js5MasterIndex.Entry(0, 5) 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( private val decodedVersioned = Js5MasterIndex(
MasterIndexFormat.VERSIONED,
mutableListOf( mutableListOf(
Js5MasterIndex.Entry(0, 1), Js5MasterIndex.Entry(0, 1, null),
Js5MasterIndex.Entry(2, 3), Js5MasterIndex.Entry(2, 3, null),
Js5MasterIndex.Entry(4, 5) 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 @Test
fun testMinimumFormat() { fun testCreateOriginal() {
assertEquals(MasterIndexFormat.ORIGINAL, decodedOriginal.minimumFormat) val index = Store.open(ROOT.resolve("original")).use { store ->
assertEquals(MasterIndexFormat.VERSIONED, decodedVersioned.minimumFormat) Js5MasterIndex.create(store)
}
assertEquals(
Js5MasterIndex(
MasterIndexFormat.ORIGINAL,
mutableListOf(
Js5MasterIndex.Entry(
0, 609698396, ByteBufUtil.decodeHexDump(
"0e1a2b93c80a41c7ad2a985dff707a6a8ff82e229cbc468f04191198920955a1" +
"4b3d7eab77a17faf99208dee5b44afb789962ad79f230b3b59106a0af892219c"
)
),
)
), index
)
} }
@Test @Test
fun testCreate() { fun testCreateVersioned() {
val index = Store.open(ROOT).use { store -> val index = Store.open(ROOT.resolve("versioned")).use { store ->
Js5MasterIndex.create(store) Js5MasterIndex.create(store)
} }
assertEquals( assertEquals(
Js5MasterIndex( Js5MasterIndex(
MasterIndexFormat.VERSIONED,
mutableListOf( mutableListOf(
Js5MasterIndex.Entry(0, 609698396), Js5MasterIndex.Entry(
Js5MasterIndex.Entry(0x12345678, 78747481), 0, 609698396, ByteBufUtil.decodeHexDump(
Js5MasterIndex.Entry(0, 0), "0e1a2b93c80a41c7ad2a985dff707a6a8ff82e229cbc468f04191198920955a1" +
Js5MasterIndex.Entry(0x9ABCDEF0.toInt(), -456081154), "4b3d7eab77a17faf99208dee5b44afb789962ad79f230b3b59106a0af892219c"
Js5MasterIndex.Entry(0, 0), )
Js5MasterIndex.Entry(0, 0), ),
Js5MasterIndex.Entry(0xAA55AA55.toInt(), 186613982) 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 testCreateWhirlpool() {
val index = Store.open(ROOT.resolve("whirlpool")).use { store ->
Js5MasterIndex.create(store)
}
assertEquals(
Js5MasterIndex(
MasterIndexFormat.WHIRLPOOL,
mutableListOf(
Js5MasterIndex.Entry(
0, 668177970, ByteBufUtil.decodeHexDump(
"2faa83116e1d1719d5db15f128eb57f62afbf0207c47bced3f558ec17645d138" +
"72f4fb9b0e36a5f6f5d30e1295b3fa49556dfd0819cb5137f3b69f64155f3fb7"
)
),
Js5MasterIndex.Entry(
0, 1925442845, ByteBufUtil.decodeHexDump(
"fcc45b0ab6d0067889e44de0004bcbb6cc538aff8f80edf1b49b583cedd73fea" +
"937ae6990235257fe8aa35c44d35450c13e670711337ee5116957cd98cc27985"
)
)
) )
), index ), index
) )
@ -73,7 +197,7 @@ object Js5MasterIndexTest {
@Test @Test
fun testWriteOriginal() { fun testWriteOriginal() {
ByteBufAllocator.DEFAULT.buffer().use { actual -> ByteBufAllocator.DEFAULT.buffer().use { actual ->
decodedOriginal.write(actual, MasterIndexFormat.ORIGINAL) decodedOriginal.write(actual)
Unpooled.wrappedBuffer(encodedOriginal).use { expected -> Unpooled.wrappedBuffer(encodedOriginal).use { expected ->
assertEquals(expected, actual) assertEquals(expected, actual)
@ -98,11 +222,102 @@ object Js5MasterIndexTest {
@Test @Test
fun testWriteVersioned() { fun testWriteVersioned() {
ByteBufAllocator.DEFAULT.buffer().use { actual -> ByteBufAllocator.DEFAULT.buffer().use { actual ->
decodedVersioned.write(actual, MasterIndexFormat.VERSIONED) decodedVersioned.write(actual)
Unpooled.wrappedBuffer(encodedVersioned).use { expected -> Unpooled.wrappedBuffer(encodedVersioned).use { expected ->
assertEquals(expected, actual) 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