I am planning to do game data mining in LOL but stuck at parsing replay files. I find that the most popular replay recorder is LOL Replay which records games in .lrf files. They are saved as binary files. I try to print a lrf file to find some patterns in it. As far as I know, the file has two parts:
The initial part is meta data. It's human readable. At the end of it, it shows an encryption key(32bytes) and a client hash for this .lrf file.
The second part has several sections. Each section is in "RESTful URL+encryption+padding(possibly)" format. For example:
?S4GI____GET /observer-mode/rest/consumer/getGameDataChunk/EUW1/1390319411/1/token
?S4GH____?¥?G??,\??1?q??"Lq}?n??&??????l??(?^P???¥I?v??k>x??Z?£??3Gug
......
??6GI____GET /observer-mode/rest/consumer/getGameDataChunk/EUW1/1390319411/2/token
Some are even unreadable characters.3
I have followed this link and this wiki. It seems like they use BlowFish ECB Algorithm plus PKCS5Padding to encrypt after using GZIP to compress contents. But I failed to decrypt contents using the 32 bytes encryptionkey in meta data. And I am not sure where I should start to read and where to stop because JVM keeps warning me that Given final block not properly padded.
So my question is:
Is there any one who is familiar with Blowfish Algorithm and PKCS5Padding? Which part of those binary files should I read to decrypt between two consecutive RESTful URL? Do I use the right key to decrypt? (the 32 bytes encryption key in the meta data)
Given the patterns around each RESRful URL, could anyone make a guess which algorithm exactly LOL uses to encrypt/decrypt contents? Is it Blowfish algorithm?
Any help would be appreciated. Thank you guys.
Edit #6.17:
Following Divis and avbor's answers, I tried the following Java snippet to decode chunks:
// Decode EncryptKey with GameId
byte[] gameIdBytes = ("502719605").getBytes();
SecretKeySpec gameIdKeySpec = new SecretKeySpec(gameIdBytes, "Blowfish");
Cipher gameIdCipher = Cipher.getInstance("Blowfish/ECB/PKCS5Padding");
gameIdCipher.init(Cipher.DECRYPT_MODE, gameIdKeySpec);
byte[] encryptKeyBytes = Base64.decode("Sf9c+zGDyyST9DtcHn2zToscfeuN4u3/");
byte[] encryptkeyDecryptedByGameId = gameIdCipher.doFinal(encryptKeyBytes);
// Initialize the chunk cipher
SecretKeySpec chunkSpec = new SecretKeySpec(encryptkeyDecryptedByGameId, "Blowfish");
Cipher chunkCipher = Cipher.getInstance("Blowfish/ECB/PKCS5Padding");
chunkCipher.init(Cipher.DECRYPT_MODE, chunkSpec);
byte[] chunkContent = getChunkContent();
byte[] chunkDecryptedBytes = chunkCipher.doFinal(chunkContent);
It works with no error when decoding encryptionkey with gameid. However it doesn't work in the last two lines. Currently I just hard coded getChunkContent() to return an byte array containing the bytes between two RESTful URLs. But Java either returns "Exception in thread "main" javax.crypto.IllegalBlockSizeException: Input length must be multiple of 8 when decrypting with padded cipher"
Or
returns "Exception in thread "main" javax.crypto.BadPaddingException: Given final block not properly padded".
I notice that the hex pattern between two RESTful URLs are as follows:
(hex for first URL e.g. /observer-mode/rest/consumer/getKeyFrame/EUW1/502719605/2/token) + 0a + (chunk contents) + 000000 + (hex for next URL)
My questions are:
Which part of chunks need to be included? Do I need to include "0a" right after the last URL? Do I need to include "000000" before the next URL?
Am I using the right padding algorithm (Blowfish/ECB/PKCS5Padding)?
My test lrf file could be downloaded on : https://www.dropbox.com/s/yl1havphnb3z86d/game1.lrf
EDIT # 6.18
Thanks to Divis! Using the snippet above, I successfully got some chunk info decrypted without error. Two things worth noting when you write your own getChunkContent():
The chunk content starts right after "hex for previous url 0a".
The chunk content ends as close as possible to "0000000 (hex for next url)" when its size reaches exactly a multiple of 8.
But I still got two questions to ask:
Here is an example of what I decode for the content between two .../getKeyframe/... RESTful urls.
39117e0cc2f7e4bb1f8b080000000000000bed7d0b5c15d5 ... 7f23a90000
I know Gzip compressed data starts with "1f8b08..." according to this RFC doc. Can I just discard "39117e0cc2f7e4bb" and start gzip decompress the proceeding content? (Actually I've already tried to start decoding from "1f8b08..", at least it could be decompressed without error)
After the gzip decompression, the result is still a long sequence of binary (with some readable strings like summoners names, champions names, etc.) When I look at the wiki, it seems like it is far from finish. What I expect is to read every item, rune, or movement in readable string. How exactly can I read those game events from it? Or we just need some patience to figure them out ourselves with the community?
Millions of thanks!
Repository dev contributor here, according to the wiki, the key is the base64 Blowfish ECB "encryption_key" (with game id as key for the blowfish).
Then, use this decrypted key to decode the content (blow fish ECB too). Then, gzip decode.
base64decode encryptionkey = decodedKey
blowfishECBdecode decodedKey with (string) gameId as key = decodedKey
blowfishECBdecode content with decodedKey as key = decodedContent
gzipdecode decodedContent = binary
I made a library to download and decode replay files : https://github.com/EloGank/lol-replay-downloader and the CLI command is also available : https://github.com/EloGank/lol-replay-downloader-cli
Hope it'll help :)
To my knowledge, you decrypt the chunks and keyframes using Blowfish. In order to get the key to decrypt said chunks and keyframes, you take the given encryption key, base64 encode it, and then use Blowfish on that using the game id as the key in order to get the actual encryption key for the chunks and keyframes.
Related
I am newbie in regards to AES encryption/decryption.
I found the following nice article from Michael Remijan
https://www.javacodegeeks.com/2017/12/choosing-java-cryptographic-algorithms-part-2-single-key-symmetric-encryption.html
I have tried the example tests, and it runs just fine whith smaller length passwords.
However when I change the length of the message (password) string to a length >= 16 it fails with the following exception.
javax.crypto.AEADBadTagException: Tag mismatch!
at com.sun.crypto.provider.GaloisCounterMode.decryptFinal(GaloisCounterMode.java:578)
at com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1049)
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:985)
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:847)
at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
at javax.crypto.Cipher.doFinal(Cipher.java:2047)
I have contacted Michael about this issue, yet he does not currently know why this behaviour occurs.
However I recently just found a fix for this but I unfortunately do not understand why. I hope you guys can help here :-)
The fix looks like this and is to be found in the encrypt method of the Aes class.
The original encrypt method in the Aes class (from the article) contains the following :
public byte[] encrypt(String message, Optional<String> aad) {
......
// Add message to encrypt
c.update(message.getBytes("UTF-8"));
// Encrypt
byte[] encryptedBytes = c.doFinal();
//....
I changed it to this :
public byte[] encrypt(String message, Optional<String> aad) {
.....
// Add message to encrypt
// REMOVE THIS c.update(message.getBytes("UTF-8"));
// Encrypt
byte[] encryptedBytes = c.doFinal(message.getBytes("UTF-8"));
.....
and it works :-) but why ?
Thanks alot.
I have updated my original blog post to fix the error Peter found.
https://mjremijan.blogspot.com/2017/12/choosing-java-cryptographic-algorithms_22.html
For demonstration purposes, I made my Aes class abstract with two new concrete classes which extend it:
AesUsingSinglePartEncryption.java - Demonstrates the single-part encryption operation, which uses a single call to Cipher#doFinal(byte[]) to get the encrypted bytes.
AesUsingMultiplePartEncryption.java - Demonstrates the multiple-part encryption operation which makes multiple calls to Cipher#update(byte[]) and then a final call to Cipher#doFinal(). All the encrypted bytes are store along the way and returned after the call to Cipher#doFinal().
This solves the problem and demonstrates two different ways of using Cipher to get encrypted bytes.
I am trying to encrypt a string with php open_ssl and then decrypt it with Java. I thought I kind of understood what was going on, but apparently not.
At first I was unable to get the algorithms to match up. From what I can gather, openssl_private_encrypt() is using RSA and although the documentation is about PKCS1_PADDING, from what I read it seems that it was changed to use PKCS5/7 to become more secure. And I cannot get any Java cipher with RSA/NONE/PKCS5 or PKCS7.
But I thought I was having success by using NoPadding, and filling the block myself. This is with an existing 512 bit key that I converted from DER to PEM with openssl. I had a test string of
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
I was able to read in the private key in php and encrypt the text with
$fp=fopen("/folder/private_key.pem","r");
$privkey_res=fread($fp,1024);
$privkey = openssl_pkey_get_private($privkey_res);
$padding = OPENSSL_NO_PADDING;
openssl_private_encrypt($texttocrypt, $encryptedtext, $privkey, $padding);
file_put_contents("/folder/encrypted.txt", $encryptedtext );
Then back in Java I then was able to correctly decrypt that string using
Cipher cipherb = Cipher.getInstance("RSA/NONE/NoPadding");
cipherb.init(Cipher.DECRYPT_MODE, publicKey);
decrypted = cipherb.doFinal(text.getBytes());
So I thought I could get things working to be useful. However, then I changed the test string slightly, like the last 'f' to 'g'
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdeg
And then I got complete garbage on the decryption. Although no error. And if I just changed the first character, it still decrypted correctly.
At this point I am not even sure what it is that I don't understand. But is there a way to do my original goal? Encrypt with php open_ssl and decrypt with Java.
Thanks
I am trying to make a call to a ws-security secured webservice from a server which unfortunately does not support this natively. The approach I have taken is to implement a .jsp which acts as reverse proxy to the actual end point URL, in the process adding the element with ws-security elements.
This seems to be working quite well and I am confident I've constructed the XML correctly with the correct namespaces etc. I've verified this by comparing the XML with XML produced by SOAP-UI.
The problem is in implementing the password digest generator. I don't get the same result as what SOAP-UI does using the same inputs for NOnce, xsd:dateTime and password, and the following code.
StringBuffer passwordDigestStr_ = new StringBuffer();
// First append the NOnce from the SOAP header
passwordDigestStr_.append(Base64.decode("PzlbwtWRpmFWjG0JRIRn7A=="));
// Then append the xsd:dateTime in UTC timezone
passwordDigestStr_.append("2012-06-09T18:41:03.640Z");
// Finally append the password/secret
passwordDigestStr_.append("password");
System.out.println("Generated password digest: " + new String(com.bea.xbean.util.Base64.encode(org.apache.commons.codec.digest.DigestUtils.sha(passwordDigestStr_.toString())), "UTF-8"));
I think the problem is with implementing the hashing of the first two elements as explained by http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0.pdf
Note that the nonce is hashed using the octet sequence of its decoded value while the timestamp is hashed using the octet sequence of its UTF8 encoding as specified in the contents of the element.
If anyone could help me solve this problem that would be great because it's beginning to drive me crazy! It would be ideal if you could provide source code.
I'll take a crack at it without SOAP-UI. The input to the hash function is supposed to be bytes, not a string. DigestUtils.sha() will allow you to use a string, but that string must be properly encoded. When you wrote the nonce, you were calling StringBuffer.append(Object) which ends up calling byte[].toString(). That gives you something like [B#3e25a5, definitely not what you want. By using bytes everywhere, you should avoid this problem. Note that the example below uses org.apache.commons.codec.binary.Base64, not the Base64 class you were using. It doesn't matter, that's just the one I had handy.
ByteBuffer buf = ByteBuffer.allocate(1000);
buf.put(Base64.decodeBase64("PzlbwtWRpmFWjG0JRIRn7A=="));
buf.put("2012-06-09T18:41:03.640Z".getBytes("UTF-8"));
buf.put("password".getBytes("UTF-8"));
byte[] toHash = new byte[buf.position()];
buf.rewind();
buf.get(toHash);
byte[] hash = DigestUtils.sha(toHash);
System.out.println("Generated password digest: " + Base64.encodeBase64String(hash));
Apologies for the delay in replying, especially considering your initial quick response. I have now been able to get this to work using the essence of your approach to avoid any character encoding issues. However, java.nio.ByteBuffer caused me issues so I modified the code to use basic byte[]s which I combined using System.arrayCopy(). The problem I faced with java.nio.ByteBuffer was that despite 'buf.position()' returning an appropriate number of bytes, all the bytes injected into byte[] toHash through buf.get(toHash) were 0s!
Thanks very much for your assistance.
I've been writing a Web Application recently that interacts with iPhones. The iPhone iphone will actually send information to the server in the form of a plist. So it's not uncommon to see something like...
<key>RandomData</key>
<data>UW31vrxbUTl07PaDRDEln3EWTLojFFmsm7YuRAscirI=</data>
Now I know this data is hashed/encrypted in some fashion. When I open up the plist with an editor (Property List Editor), it shows me a more "human readable" format. For example, the data above would be converted into something like...
<346df5da 3c5b5259 74ecf683 4431249f 711630ba 232c54ac 9bf2ee44 0r1c8ab2>
Any idea what the method of converting it is? Mainly I'm looking to get this into a Java String.
Thanks!
According to our friends at wikipedia, the <data> tag contains Base64 encoded data. So, use your favorite Java "Base64" class to decode (see also this question).
ps. technically, this is neither "hashed" nor "encrypted", simply "encoded". "Hashed" implies a one-way transformation where multiple input values can yield the same output value. "Encrypted" implies the need for a (usually secret) "key" to reverse the encryption. Base64 encoding is simply a way of representing arbitrary binary data using only printable characters.
After base64 decoding it you need to hex encode it. This is what PL Editor is showing you.
So...
<key>SomeData</key>
<data>UW31ejxbelle7PaeRAEen3EWMLojbFmsm7LuRAscirI=</data?
Can be represented with...
byte[] bytes = Base64.decode("UW31ejxbelle7PaeRAEen3EWMLojbFmsm7LuRAscirI=");
BigInteger bigInt = new BigInteger(bytes);
String hexString = bigInt.toString(16);
System.out.println(hexString);
To get...
<516df5aa 3c5b5259 74ecf683 4401259f 711630ba 236c59ac 9bb2ee44 0b1c8ab2>
I am working on encryption-decryption program.
Program gets an input from the user and encrypts it. Then it stores the encrypted data in ms access database table.
Later, the data is retrieved from the table , decrypted and given back to the user.
I am storing the data as text in the ms access. The encryption algorithm returns a byte array of size 16.
But when i retrieve the data from the database, i am getting a byte array of size 8 only.
Help me to get through this...
I think the problem is that you are using it as text while it isn't (it is binary data). The halving of the length sounds like a Unicode related issue (i.e. the 'text' is stored as wide with two bytes for character, but retrieved as one byte per character).
I have an app that stores encrypted credit card numbers using the MS Crypto interface. I got the code from the MS Knowledge Base, and the key thing is running ByteToString() and StringToByte() conversion in the proper places. I'm storing the actual data in a plain Jet text field and have had no problems whatsoever.
one possible solution is to encode the cipher text as String using Base64 encoding
you can use Appache Commons Library for that:
http://commons.apache.org/codec/apidocs/org/apache/commons/codec/binary/Base64.html
Edited:
i dont know why you want MS-ACCESS Specific solution ! the DMBS may change, the OS also may change.. you must to write general solution that can work in many cases..
here small example for using Base64 Encoder/Decoder:
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import java.io.IOException ;
public class Decoder {
public static void main(String[] args) throws IOException{
byte[] cipherBytes = "stackoverflow".getBytes(); // say this the is encrypted bytes
String encodedBytes = new BASE64Encoder().encode(cipherBytes);
System.out.println("stored as: " + encodedBytes );
byte[] decodedBytes = new BASE64Decoder().decodeBuffer(encodedBytes);
System.out.println("extracted as: " + new String(decodedBytes) );
}
}
Note: this code using Internal Sun Classes (BASE64Encoder/Decoder) and its not recommended to use these classes in your program because it may change in the next version of JDK.
using BASE64 Encoder/Decoder in Appache Commons is better.
if you want the MS-ACCESS solution, try to store the ciphertext in LONGBINARY , see this:
How to specify blob type in MS Access?