Closed fivedashthree closed 7 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.
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!
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.
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?
I'm pretty sure that UpdateSharedKey
is doing its job properly, because the proxy works fine with it.
What
"
crypto_box_beforenm
usingsk
andserverkey
"
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.
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.
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.
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;
.
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.
Wow! Massive commits! Awesome work :smile: I'm gonna dive into this and mess around. Thankyou so much!
I guess we can close this since we've figured it out.
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.
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.
Ah, didn't know that was there! I got the latest one from APKMirror anyway.
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:
I'm trying to follow the Protocol page on clugh's wiki, but I'm not sure where I plug in the client-generated
pk
andsk
keys 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.