FICTURE7 / CoCSharp

Clash of Clans library, proxy and server written in .NET [Unmaintained]
MIT License
109 stars 57 forks source link

CoCSharp.Client 8.x encryption support #90

Closed fivedashthree closed 7 years ago

fivedashthree commented 8 years ago

The CoCSharp.Client project hasn't been updated for nearly a year and won't work with the new encryption system.

I've started writing a new client, but I can't quite get the encryption right. The CoCSharp library seems to only have partial support for client -> server communication.

Here's what I have so far. I'm sucessfully sending 10100 and getting 20100 in return, but I get no reply after sending 10101, which suggests that the message is not even being read, so probably bad encryption.

Here's the relevant part of the code:

        HandshakeSuccessMessage reply = (HandshakeSuccessMessage)Messages.Dequeue();
        byte[] SessionKey = reply.SessionKey;

        //Generate nonce and client keys
        byte[] snonce = Crypto8.GenerateNonce();

        CoCKeyPair pksk = Crypto8.GenerateKeyPair();
        byte[] pk = pksk.PrivateKey;
        byte[] sk = pksk.PublicKey;

        //byte[] nonce = Crypto8.GenerateBlake2BNonce(pk, Crypto8.SupercellPublicKey);
        crypto.UpdateSharedKey(Crypto8.SupercellPublicKey);

        crypto.UpdateNonce(snonce, UpdateNonceType.Blake);
        crypto.UpdateNonce(snonce, UpdateNonceType.Encrypt);

        //Send LoginRequestMessage (id: 10101)`

I'm trying to follow the Protocol page on clugh's wiki, but I'm not sure where I plug in the client-generated pkand skkeys from step 6.

I'm fairly certain that I'm doing something wrong there, but if that's not the issue, CoCSharp doesnt seem to have proper support for sending message 10101 anyway, as it needs to be prefixed with the client's pk after encryption.

FICTURE7 commented 8 years ago

My implementation of the 8.x.x Clash of Clans encryption is weird, that is Crypto8. It makes use of previous arguments passed into UpdateSharedKey and UpdateNonce to figure out what keys and what nonce to use.

And yes, you're right the NetworkManagerAsync supports server to client connection only at the moment. But am in the process of changing the way the NetworkManagerAsync handles incoming and outgoing messages to be more flexible.

But you can always look in how I made the proxy, because the proxy is both a client and a server.

fivedashthree commented 8 years ago

Right. I'll try to just write the login process from scratch then (similarly to the proxy), that way it's easier to see exactly what's being done.

I'll have to add in a specific case for packet 10101 in NetworkManagerAsync though. Once I get the encryption right, I'll get back to you and send a pull request

I did look at the proxy :) it's much more simple though, as it doesn't have to do the encryption processing, it mostly just forwards messages on using differnet keys.

Thanks!

FICTURE7 commented 8 years ago

By the way, the client should initialize the Crypto8 with a generated key pair. Therefore new Crypto8(MessageDirection.Server); should be used. Its this generated key pair that is going to be pk and sk where pk is the public key and sk the private key.

You should then do crypto.UpdateSharedKey(Crypto8.SupercellPublicKey), this will update the Crypto8 and it will generate the blake 2b nonce and use with it Encrypt or Decrypt.

The client then encrypts LoginRequestMessage with the SupercellPublicKey, its sk and the blake 2b nonce which gets generated this way, nonce = blake2b(pk, SupercellPublicKey) which is what the Crypto8 does when calling crypto.UpdateSharedKey(Crypto8.SupercellPublicKey). Then after it has encrypted the message it appends its generated pk, in front of the encrypted message bytes/data.

The server will then read the unencrypted pk which is in front of the encrypted LoginRequestMessage. Generate the nonce the same way the client did it, that is nonce = blake2b(pk, SupercellPublicKey). Then decrypt the encrypted LoginRequestMessage using its private key (which by the way, is why we need to overwrite the key in libg.so to make the client work on non-official servers), pk and the generated blake 2b nonce.

And also by the way, the proxy is going to get smashed into peaces in the next commit as well as NetworkManagerAsync wont have almost any message processing into it. Instead it is going to use a MessageProcessor to process incoming and outgoing messages.

fivedashthree commented 7 years ago

That was an amazing help, thanks! The whole process makes much more sense.

The keys are now generated by the client, I've sorted that. I added in a special case for LoginRequestMessage, after encryption:

if (message is LoginRequestMessage)
            {
                // Prefix the encrypted data with the client's generated key (pk) so the server can decrypt it
                var lrMessage = message as LoginRequestMessage;
                byte[] newMessageData = new byte[body.Length + lrMessage.PublicKey.Length];
                Buffer.BlockCopy(lrMessage.PublicKey, 0, newMessageData, 0, lrMessage.PublicKey.Length);
                Buffer.BlockCopy(body, 0, newMessageData, lrMessage.PublicKey.Length, body.Length);
                body = newMessageData;
            }

Everything seems nice ... except I'm still gettting no response from the server. After reviewing each step, I think that I've found the problem. crypto.UpdateSharedKey(SupercellPublicKey) generates a nonce and sets _sharedkey to serverkey (that is, instead of generating a new shared key, it just takes serverkey), but the Protocol page sasy that we should be generating the shared key with "crypto_box_beforenm using sk and serverkey". What's the equivalent of that function in libsodium-net?

edit: according to this StackOverflow post, crypto_box_beforenm outputs a HSalsa hash of the Curve25519 algorithm - a.k.a. Sodium.ScalarMult.Mult(secretKey, publicKey). If crypto_box_beforenm isn't exposed, can I use this instead, hasing it with Salsa20?

Also, the server seems to have the same problem, it doesn't actually generate a shared key (step 17), but still seems to work alright. Am I missing something here?

FICTURE7 commented 7 years ago

I'm pretty sure that UpdateSharedKey is doing its job properly, because the proxy works fine with it.

What

"crypto_box_beforenm using sk and serverkey"

means is that, it uses the client's generated secret key, that is sk, and use the server's public key, that is serverkey, which the SupercellPublicKey, to encrypt the data.

Which means this

// 'nonce' is generated using blake2b this way.
// nonce = blake2b(pk, serverKey);
// where pk is the generated public key, and serverKey the SupercellPublicKey.
var plaintext = PublicKeyBox.Open(chiper, nonce, sk, serverKey);

I'll try to describe with some pseudo code how to login on the server, using the Crypto8 class.

var socket = connect("gamea.clashofclans.com", 9339);

// Crypto8 will initialize its KeyPair with a random one using
// Crypto8.GenerateKeyPair();
var crypto  = new Crypto8(MessageDirection.Server);

// This generated key pair will be pk and sk.

var handshakeReq = new HandshakeRequestMessage
{
    // Fill data
};

// Sends HandshakeRequestMessage.
var hrbody = write_body(handshakeReq);
var hrheader = write_header(handshakeReq.ID, hrbody .Length, handshakeReq.Version);
var hrbytes = concat(hrheader, hrbody);
socket.Send(hrbytes);

// Recieves HandshakeResponseMessage.
var handshakeRes = socket.Receive();

var loginReq = new LoginRequestMessage
{
    // Fill data
};

// This tells the Crypto8 class to use the specified public/shared key 
// to encrypt incoming or decrypt outgoing data. That is the SupercellPublicKey.
crypto.UpdateSharedKey(Crypto8.SupercellKey);

// The Crypto8 class will then generate a blake2b nonce in the background
// with `nonce = blake2b(pk, serverkey)` where pk is the generate public key
// and serverkey is the SupercellPublicKey specified in crypto.UpdateSharedKey.

// Send LoginRequestMessage.
var lrbody = write_body(handshakeReq);
var lrbodyEn = crypto.Encrypt(ref lrbody);
var lrbodyComplete = concat(crypto.KeyPair.PublicKey, lrbodyEn);
// Make sure the length of message in header includes the public key's length.
// So message length should be `publickey.Length + enBody.Length`.
var lrheader = write_header(lrbodyComplete);
var lrbytes = concat(lrheader, lrbodyComplete);
socket.Send(lrbytes);

I've haven't tested any of this, sooo, it may or may not work.

In the latest commit I smashed everything apart regarding encryption handling in NetworkManagerAsync, but I've updated the proxy. There is less logic in the NetworkManagerAsync now.

FICTURE7 commented 7 years ago

And of course, you use Crypto8.SupercellPublicKey only if you want to connect to a server which is using and has the corresponding private key. Which probably, Supercell only has.

fivedashthree commented 7 years ago

Hmm. it must be working fine then. I'm reading things wrong :sweat_smile: my understanding was that the same shared key is generated from (sk, serverkey) on our side and (clientpublickey, serversecretkey) on their side ... ergh, i don't know.

That code is ... uhm, exactly what I'm doing (except I'm sending through networkmanagerasync). I've checked that the message is correct (well, the length is correct and the it's correctly prefixed with pk, I obviously can't decrypt it).

Yeah, I'm trying to connect to the vanilla servers. I'm still not sure what's going wrong, but I'll mess around with some more things and see what happens.

FICTURE7 commented 7 years ago

Oh, by the way another thing I noticed about your code is that msg.UserToken = BitConverter.ToString(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF });. You shouldn't do that, instead you should do msg.UserToken = null;.

FICTURE7 commented 7 years ago

Alright quick update, I took a look into the client stuff again and I found some bugs regarding the writing LoginRequestMessage. But I've fixed them and updated MessageProcessorNaCl. So login in should be really easy now, take a look here

public static void Main()
{
    var sock = new Socket(SocketType.Stream, ProtocolType.Tcp);
    sock.Connect("game.clashofclans.com", 9339);

    var net = new NetworkManagerAsync(sock, new MessageProcessorNaCl(Crypto8.GenerateKeyPair(), Crypto8.SupercellPublicKey));
    net.MessageReceived += (object sender, MessageReceivedEventArgs e) =>
    {
        if (e.Message == null)
        {
            Console.WriteLine("Unable to read message.");
            return;
        }

        Console.WriteLine("Received: {0}", e.Message.ID);
        if (e.Message is HandshakeSuccessMessage)
        {
            var message = e.Message as HandshakeSuccessMessage;
            var processor = net.Processor as MessageProcessorNaCl;
            processor.UpdateSessionKey(message.SessionKey);
            var loginReq = new LoginRequestMessage
            {
                // To make a new account
                UserID = 0,
                UserToken = null,

                MajorVersion = 8,
                MinorVersion = 551,
                ContentVersion = 0,
                LocaleKey = 2000000,
                Language = "en",
                AdvertisingGUID = "",
                OSVersion = "4.4.2",
                IsAdvertisingTrackingEnabled = true,
                MasterHash = "bfd77f9a4a1fe2bd73fc2b749869fd0f64cec65f",
                FacebookDistributionID = "",
                VendorGUID = "",
                ClientVersion = "8.551.18",
                Seed = new Random().Next()
            };
            net.SendMessage(loginReq);
        }
        else if (e.Message is LoginFailedMessage)
        {
            var message = e.Message as LoginFailedMessage;
            Console.WriteLine("Login failed: {0}", message.Reason);
        }
        else if (e.Message is LoginSuccessMessage)
        {
            Console.WriteLine("Login success!");
        }
    };
    net.Disconnected += (object sender, DisconnectedEventArgs e) =>
    {
        Console.WriteLine("Disconnected!");
    };

    var handshake = new HandshakeRequestMessage
    {
        AppStore = 2,
        Build = 551,
        DeviceType = 2,
        Hash = "bfd77f9a4a1fe2bd73fc2b749869fd0f64cec65f",
        KeyVersion = 15,
        MajorVersion = 8,
        MinorVersion = 0,
        Protocol = 1,
    };
    net.SendMessage(handshake);
    Console.ReadLine();
}

With this code I was able to log on the official server. If you want to look into how the encryption is being handled, take a look into MessageProcessorNaCl. Another thing to note is that in the HandshakeRequestMessage, the KeyVersion is 15, it was 16 in your code. When I used KeyVersion 16 the server did not respond with anything and was idle.

fivedashthree commented 7 years ago

Wow! Massive commits! Awesome work :smile: I'm gonna dive into this and mess around. Thankyou so much!

FICTURE7 commented 7 years ago

I guess we can close this since we've figured it out.

fivedashthree commented 7 years ago

Sure! Do you want me to write a more fleshed-out client to add into the project? I mean, I will anyway, for my own purposes, but I need to know if I have to make the code clean or not :)

By the way, the encryption workd fantastically in that code, but I'm getting LoginFailedMessage: OutdatedContent after the Login packet.

FICTURE7 commented 7 years ago

If you want to. ;]

You need to change the MasterHash/Hash to the latest one. You could obtain it using the LoginFailedMessage.Fingerprint.MasterHash. And of course you need to to make it into a hex-string.

fivedashthree commented 7 years ago

Ah, didn't know that was there! I got the latest one from APKMirror anyway.