package net.scapeemulator.cache; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import net.scapeemulator.cache.util.crypto.Rsa; import net.scapeemulator.cache.util.crypto.Whirlpool; /** * A {@link ChecksumTable} stores checksums and versions of * {@link ReferenceTable}s. When encoded in a {@link Container} and prepended * with the file type and id it is more commonly known as the client's * "update keys". * @author Graham * @author `Discardedx2 */ public final class ChecksumTable { /** * Decodes the {@link ChecksumTable} in the specified * {@link ByteBuffer}. Whirlpool digests are not read. * @param buffer The {@link ByteBuffer} containing the table. * @return The decoded {@link ChecksumTable}. * @throws IOException if an I/O error occurs. */ public static ChecksumTable decode(ByteBuffer buffer) throws IOException { return decode(buffer, false); } /** * Decodes the {@link ChecksumTable} in the specified * {@link ByteBuffer}. * @param buffer The {@link ByteBuffer} containing the table. * @param whirlpool If whirlpool digests should be read. * @return The decoded {@link ChecksumTable}. * @throws IOException if an I/O error occurs. */ public static ChecksumTable decode(ByteBuffer buffer, boolean whirlpool) throws IOException { return decode(buffer, whirlpool, null, null); } /** * Decodes the {@link ChecksumTable} in the specified * {@link ByteBuffer} and decrypts the final whirlpool hash. * @param buffer The {@link ByteBuffer} containing the table. * @param whirlpool If whirlpool digests should be read. * @param modulus The modulus. * @param publicKey The public key. * @return The decoded {@link ChecksumTable}. * @throws IOException if an I/O error occurs. */ public static ChecksumTable decode(ByteBuffer buffer, boolean whirlpool, BigInteger modulus, BigInteger publicKey) throws IOException { /* find out how many entries there are and allocate a new table */ int size = whirlpool ? (buffer.get() & 0xFF) : (buffer.limit() / 8); ChecksumTable table = new ChecksumTable(size); /* calculate the whirlpool digest we expect to have at the end */ byte[] masterDigest = null; if (whirlpool) { byte[] temp = new byte[size * 72 + 1]; buffer.position(0); buffer.get(temp); masterDigest = Whirlpool.whirlpool(temp, 0, temp.length); } /* read the entries */ buffer.position(whirlpool ? 1 : 0); for (int i = 0; i < size; i++) { int crc = buffer.getInt(); int version = buffer.getInt(); byte[] digest = new byte[64]; if (whirlpool) { buffer.get(digest); } table.entries[i] = new Entry(crc, version, digest); } /* read the trailing digest and check if it matches up */ if (whirlpool) { byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); ByteBuffer temp = ByteBuffer.wrap(bytes); if (modulus != null && publicKey != null) { temp = Rsa.crypt(buffer, modulus, publicKey); } if (temp.limit() != 65) throw new IOException("Decrypted data is not 65 bytes long"); for (int i = 0; i < 64; i++) { if (temp.get(i + 1) != masterDigest[i]) throw new IOException("Whirlpool digest mismatch"); } } /* if it looks good return the table */ return table; } /** * Represents a single entry in a {@link ChecksumTable}. Each entry * contains a CRC32 checksum and version of the corresponding * {@link ReferenceTable}. * @author Graham Edgecombe */ public static class Entry { /** * The CRC32 checksum of the reference table. */ private final int crc; /** * The version of the reference table. */ private final int version; /** * The whirlpool digest of the reference table. */ private final byte[] whirlpool; /** * Creates a new entry. * @param crc The CRC32 checksum of the slave table. * @param version The version of the slave table. * @param whirlpool The whirlpool digest of the reference table. */ public Entry(int crc, int version, byte[] whirlpool) { if (whirlpool.length != 64) throw new IllegalArgumentException(); this.crc = crc; this.version = version; this.whirlpool = whirlpool; } /** * Gets the CRC32 checksum of the reference table. * @return The CRC32 checksum. */ public int getCrc() { return crc; } /** * Gets the version of the reference table. * @return The version. */ public int getVersion() { return version; } /** * Gets the whirlpool digest of the reference table. * @return The whirlpool digest. */ public byte[] getWhirlpool() { return whirlpool; } } /** * The entries in this table. */ private Entry[] entries; /** * Creates a new {@link ChecksumTable} with the specified size. * @param size The number of entries in this table. */ public ChecksumTable(int size) { entries = new Entry[size]; } /** * Encodes this {@link ChecksumTable}. Whirlpool digests are not encoded. * @return The encoded {@link ByteBuffer}. * @throws IOException if an I/O error occurs. */ public ByteBuffer encode() throws IOException { return encode(false); } /** * Encodes this {@link ChecksumTable}. * @param whirlpool If whirlpool digests should be encoded. * @return The encoded {@link ByteBuffer}. * @throws IOException if an I/O error occurs. */ public ByteBuffer encode(boolean whirlpool) throws IOException { return encode(whirlpool, null, null); } /** * Encodes this {@link ChecksumTable} and encrypts the final whirlpool hash. * @param whirlpool If whirlpool digests should be encoded. * @param modulus The modulus. * @param privateKey The private key. * @return The encoded {@link ByteBuffer}. * @throws IOException if an I/O error occurs. */ public ByteBuffer encode(boolean whirlpool, BigInteger modulus, BigInteger privateKey) throws IOException { ByteArrayOutputStream bout = new ByteArrayOutputStream(); try (DataOutputStream os = new DataOutputStream(bout)) { /* as the new whirlpool format is more complicated we must write the number of entries */ if (whirlpool) os.write(entries.length); /* encode the individual entries */ for (Entry entry : entries) { os.writeInt(entry.getCrc()); os.writeInt(entry.getVersion()); if (whirlpool) os.write(entry.getWhirlpool()); } /* compute (and encrypt) the digest of the whole table */ if (whirlpool) { byte[] bytes = bout.toByteArray(); ByteBuffer temp = ByteBuffer.allocate(65); temp.put((byte) 0); temp.put(Whirlpool.whirlpool(bytes, 0, bytes.length)); temp.flip(); if (modulus != null && privateKey != null) { temp = Rsa.crypt(temp, modulus, privateKey); } bytes = new byte[temp.limit()]; temp.get(bytes); os.write(bytes); } byte[] bytes = bout.toByteArray(); return ByteBuffer.wrap(bytes); } } /** * Gets the size of this table. * @return The size of this table. */ public int getSize() { return entries.length; } /** * Sets an entry in this table. * @param id The id. * @param entry The entry. * @throws IndexOutOfBoundsException if the id is less than zero or greater * than or equal to the size of the table. */ public void setEntry(int id, Entry entry) { if (id < 0 || id >= entries.length) throw new IndexOutOfBoundsException(); entries[id] = entry; } /** * Gets an entry from this table. * @param id The id. * @return The entry. * @throws IndexOutOfBoundsException if the id is less than zero or greater * than or equal to the size of the table. */ public Entry getEntry(int id) { if (id < 0 || id >= entries.length) throw new IndexOutOfBoundsException(); return entries[id]; } }