npetrovski / l2js-client

JavaScript client for Lineage 2
MIT License
109 stars 36 forks source link

BlowfishEngine #6

Closed alois-git closed 4 years ago

alois-git commented 4 years ago

Hi,

I would like to understand is the BlowfishEngine unique to Lineage 2 ? Couldn't you use a existing BlowfishEngine from a open source crypto library ?

Thanks,

Aloïs

npetrovski commented 4 years ago

the engine itself is not unique and you should be able to use it from the existing crypto lib, it just that I was trying to minimize the project dependencies and get rid of all extra code I don't use (crypto lib comes with much more algorithms I dont not need for the project)

alois-git commented 4 years ago

H,

Ok do you know which algorithm it uses ? blowfish_ecb, blowfish_cbc, blowfish_cfb64,blowfish_ofb64 ?

BTW thanks a lot for the doc on this project, we are missing this kind of documentation to really understand how it works. I will likely create a pull request to add more documentation.

npetrovski commented 4 years ago

NewCrypt class uses Blowfish cipher with ECB processing.

Hope this answers the question.

alois-git commented 4 years ago

Yes thanks a lot I am trying to implement NewCrypt in Elrang but I fail miserably lol

npetrovski commented 4 years ago

I had to rewrite some code from .NET and Java to JS to make it work - there are differences when it comes to a simple bit-shift operation with signed and unsigned integers. Try look at the "<<" and "<<<" operators

npetrovski commented 4 years ago

you can try use as an example the Java code like:

public final class NewCrypt {
    private final BlowfishEngine _cipher;

    /**
     * @param blowfishKey
     */
    public NewCrypt(byte[] blowfishKey) {
        _cipher = new BlowfishEngine();
        _cipher.init(blowfishKey);
    }

    public NewCrypt(String key) {
        this(key.getBytes());
    }

    /**
     * Equivalent to calling {@link #verifyChecksum(byte[], int, int)} with parameters (raw, 0, raw.length)
     * @param raw data array to be verified
     * @return true when the checksum of the data is valid, false otherwise
     */
    public static boolean verifyChecksum(final byte[] raw) {
        return NewCrypt.verifyChecksum(raw, 0, raw.length);
    }

    /**
     * Method to verify the checksum of a packet received by login server from game client.<br>
     * This is also used for game server <-> login server communication.
     * @param raw data array to be verified
     * @param offset at which offset to start verifying
     * @param size number of bytes to verify
     * @return true if the checksum of the data is valid, false otherwise
     */
    public static boolean verifyChecksum(final byte[] raw, final int offset, final int size) {
        // check if size is multiple of 4 and if there is more then only the checksum
        if (((size & 3) != 0) || (size <= 4)) {
            return false;
        }

        long chksum = 0;
        int count = size - 4;
        long check = -1;
        int i;

        for (i = offset; i < count; i += 4) {
            check = raw[i] & 0xff;
            check |= (raw[i + 1] << 8) & 0xff00;
            check |= (raw[i + 2] << 0x10) & 0xff0000;
            check |= (raw[i + 3] << 0x18) & 0xff000000;

            chksum ^= check;
        }

        check = raw[i] & 0xff;
        check |= (raw[i + 1] << 8) & 0xff00;
        check |= (raw[i + 2] << 0x10) & 0xff0000;
        check |= (raw[i + 3] << 0x18) & 0xff000000;

        return check == chksum;
    }

    /**
     * Equivalent to calling {@link #appendChecksum(byte[], int, int)} with parameters (raw, 0, raw.length)
     * @param raw data array to compute the checksum from
     */
    public static void appendChecksum(final byte[] raw) {
        NewCrypt.appendChecksum(raw, 0, raw.length);
    }

    /**
     * Method to append packet checksum at the end of the packet.
     * @param raw data array to compute the checksum from
     * @param offset offset where to start in the data array
     * @param size number of bytes to compute the checksum from
     */
    public static void appendChecksum(final byte[] raw, final int offset, final int size) {
        long chksum = 0;
        int count = size - 4;
        long ecx;
        int i;

        for (i = offset; i < count; i += 4) {
            ecx = raw[i] & 0xff;
            ecx |= (raw[i + 1] << 8) & 0xff00;
            ecx |= (raw[i + 2] << 0x10) & 0xff0000;
            ecx |= (raw[i + 3] << 0x18) & 0xff000000;

            chksum ^= ecx;
        }

        ecx = raw[i] & 0xff;
        ecx |= (raw[i + 1] << 8) & 0xff00;
        ecx |= (raw[i + 2] << 0x10) & 0xff0000;
        ecx |= (raw[i + 3] << 0x18) & 0xff000000;

        raw[i] = (byte) (chksum & 0xff);
        raw[i + 1] = (byte) ((chksum >> 0x08) & 0xff);
        raw[i + 2] = (byte) ((chksum >> 0x10) & 0xff);
        raw[i + 3] = (byte) ((chksum >> 0x18) & 0xff);
    }

    /**
     * Packet is first XOR encoded with <code>key</code> then, the last 4 bytes are overwritten with the the XOR "key".<br>
     * Thus this assume that there is enough room for the key to fit without overwriting data.
     * @param raw The raw bytes to be encrypted
     * @param key The 4 bytes (int) XOR key
     */
    public static void encXORPass(byte[] raw, int key) {
        NewCrypt.encXORPass(raw, 0, raw.length, key);
    }

    /**
     * Packet is first XOR encoded with <code>key</code> then, the last 4 bytes are overwritten with the the XOR "key".<br>
     * Thus this assume that there is enough room for the key to fit without overwriting data.
     * @param raw The raw bytes to be encrypted
     * @param offset The beginning of the data to be encrypted
     * @param size Length of the data to be encrypted
     * @param key The 4 bytes (int) XOR key
     */
    public static void encXORPass(byte[] raw, final int offset, final int size, int key) {
        int stop = size - 8;
        int pos = 4 + offset;
        int edx;
        int ecx = key; // Initial xor key

        while (pos < stop) {
            edx = (raw[pos] & 0xFF);
            edx |= (raw[pos + 1] & 0xFF) << 8;
            edx |= (raw[pos + 2] & 0xFF) << 16;
            edx |= (raw[pos + 3] & 0xFF) << 24;

            ecx += edx;

            edx ^= ecx;

            raw[pos++] = (byte) (edx & 0xFF);
            raw[pos++] = (byte) ((edx >> 8) & 0xFF);
            raw[pos++] = (byte) ((edx >> 16) & 0xFF);
            raw[pos++] = (byte) ((edx >> 24) & 0xFF);
        }

        raw[pos++] = (byte) (ecx & 0xFF);
        raw[pos++] = (byte) ((ecx >> 8) & 0xFF);
        raw[pos++] = (byte) ((ecx >> 16) & 0xFF);
        raw[pos++] = (byte) ((ecx >> 24) & 0xFF);
    }

    /**
     * Method to decrypt using Blowfish-Blockcipher in ECB mode.<br>
     * The results will be directly placed inside {@code raw} array.<br>
     * This method does not do any error checking, since the calling code<br>
     * should ensure sizes.
     * @param raw the data array to be decrypted
     * @param offset the offset at which to start decrypting
     * @param size the number of bytes to be decrypted
     */
    public void decrypt(byte[] raw, final int offset, final int size) {
        for (int i = offset; i < (offset + size); i += 8) {
            _cipher.decryptBlock(raw, i);
        }
    }

    /**
     * Method to encrypt using Blowfish-Blockcipher in ECB mode.<br>
     * The results will be directly placed inside {@code raw} array.<br>
     * This method does not do any error checking, since the calling code should ensure sizes.
     * @param raw the data array to be decrypted
     * @param offset the offset at which to start decrypting
     * @param size the number of bytes to be decrypted
     */
    public void crypt(byte[] raw, final int offset, final int size) {
        for (int i = offset; i < (offset + size); i += 8) {
            _cipher.encryptBlock(raw, i);
        }
    }
}
alois-git commented 4 years ago

Thx ! I have hardcoded parameter in my test loginserver in order to get the same packet everytime.

I saw that the decrypted content on Erlang does not match the decrypted content in the Java Client so I guess I will have to investigate...

alois-git commented 4 years ago

Ok I found out why the decryption was not working, the BlowfishEngine that we have there use little endian formating (I think). When you compare the actual implementation of BouncyCastel with the one here you can see a very small difference in these two methods:

Method from This blowfish engine

private int bytesTo32bits(byte[] b, int i) {
        return ((b[i + 3] & 0xff) << 24) | ((b[i + 2] & 0xff) << 16) | ((b[i + 1] & 0xff) << 8)
                | ((b[i] & 0xff));
    }

    private void bits32ToBytes(int in, byte[] b, int offset) {
        b[offset] = (byte) in;
        b[offset + 1] = (byte) (in >> 8);
        b[offset + 2] = (byte) (in >> 16);
        b[offset + 3] = (byte) (in >> 24);
    }

Method from actual bouncy castle engine

private int BytesTo32bits(byte[] b, int i)
    {
        return ((b[i]   & 0xff) << 24) | 
             ((b[i+1] & 0xff) << 16) |
             ((b[i+2] & 0xff) << 8) |
             ((b[i+3] & 0xff));
    }

    private void Bits32ToBytes(int in,  byte[] b, int offset)
    {
        b[offset + 3] = (byte)in;
        b[offset + 2] = (byte)(in >> 8);
        b[offset + 1] = (byte)(in >> 16);
        b[offset]     = (byte)(in >> 24);
    }
}

This was hard to spot but make a huge difference. To fix that I have to change the byte order for the input and the output going-in and coming from the bouncy castel blowfish engine in order to get the same result.

blowfishEngineBC.processBlock(swapByte(data2), 0, out2, 0);
        System.out.println("Result from blowfish engine BC " + Arrays.toString(swapByte(out2)));
npetrovski commented 4 years ago

Very good. Thanks @alokhan

lebedynskyi commented 3 years ago

Oh My Lord!! I Spent 2 days try to use default Blowfish encryption in Java / Kotlin.. The answer of @alokhan is great.. Thank you dude..