diff --git a/RustPlusApi/Examples/Fcm/FcmListener/Program.cs b/RustPlusApi/Examples/Fcm/FcmListener/Program.cs index 1fb1b9f..3ff84b8 100644 --- a/RustPlusApi/Examples/Fcm/FcmListener/Program.cs +++ b/RustPlusApi/Examples/Fcm/FcmListener/Program.cs @@ -1,6 +1,5 @@ using RustPlusApi.Fcm; using RustPlusApi.Fcm.Data; -using Newtonsoft.Json; var credentials = new Credentials { @@ -28,9 +27,7 @@ listener.NotificationReceived += (_, message) => { - var rustPlusMessage = JsonConvert.DeserializeObject(message); - var formattedMessage = JsonConvert.SerializeObject(rustPlusMessage, Formatting.Indented); - Console.WriteLine(formattedMessage); + Console.WriteLine(message); }; await listener.ConnectAsync(); \ No newline at end of file diff --git a/RustPlusApi/RustPlusApi.Fcm/Data/Credentials.cs b/RustPlusApi/RustPlusApi.Fcm/Data/Credentials.cs index d779b88..e73a3e2 100644 --- a/RustPlusApi/RustPlusApi.Fcm/Data/Credentials.cs +++ b/RustPlusApi/RustPlusApi.Fcm/Data/Credentials.cs @@ -1,46 +1,30 @@ -using Newtonsoft.Json; - -namespace RustPlusApi.Fcm.Data +namespace RustPlusApi.Fcm.Data { public sealed class Credentials { - [JsonProperty(PropertyName = "keys")] public Keys Keys { get; set; } = null!; - [JsonProperty(PropertyName = "fcm")] public FcmCredentials Fcm { get; set; } = null!; - [JsonProperty(PropertyName = "gcm")] public GcmCredentials Gcm { get; set; } = null!; } public sealed class Keys { - [JsonProperty(PropertyName = "privateKey")] public string PrivateKey { get; set; } = null!; - - [JsonProperty(PropertyName = "publicKey")] public string PublicKey { get; set; } = null!; - - [JsonProperty(PropertyName = "authSecret")] public string AuthSecret { get; set; } = null!; } public sealed class FcmCredentials { - [JsonProperty(PropertyName = "token")] public string Token { get; set; } = null!; - [JsonProperty(PropertyName = "pushSet")] public string PushSet { get; set; } = null!; } public sealed class GcmCredentials { - [JsonProperty(PropertyName = "token")] public string Token { get; set; } = null!; - [JsonProperty(PropertyName = "androidId")] public ulong AndroidId { get; set; } - [JsonProperty(PropertyName = "securityToken")] public ulong SecurityToken { get; set; } - [JsonProperty(PropertyName = "appId")] public string AppId { get; set; } = null!; } } diff --git a/RustPlusApi/RustPlusApi.Fcm/FcmListener.cs b/RustPlusApi/RustPlusApi.Fcm/FcmListener.cs index f98965b..334917a 100644 --- a/RustPlusApi/RustPlusApi.Fcm/FcmListener.cs +++ b/RustPlusApi/RustPlusApi.Fcm/FcmListener.cs @@ -5,6 +5,8 @@ using McsProto; +using Newtonsoft.Json; + using ProtoBuf; using RustPlusApi.Fcm.Data; @@ -13,7 +15,6 @@ using static RustPlusApi.Fcm.Data.Constants; using static System.GC; - namespace RustPlusApi.Fcm { public class FcmListener(Credentials credentials, ICollection persistentIds) : IDisposable @@ -59,10 +60,10 @@ public async Task ConnectAsync() UseRmq2 = true, Settings = { new Setting() { Name = "new_vc", Value = "1" } }, ClientEvents = { new ClientEvent() }, + ReceivedPersistentIds = { }, }; - if (persistentIds != null) - loginRequest.ReceivedPersistentIds.AddRange(persistentIds); + if (persistentIds != null) loginRequest.ReceivedPersistentIds.AddRange(persistentIds); SendPacket(loginRequest); @@ -72,7 +73,7 @@ public async Task ConnectAsync() Connected?.Invoke(this, EventArgs.Empty); StatusCheck(); - await ReceiveMessagesAsync(); + ReceiveMessages(); } catch (Exception ex) { @@ -91,9 +92,9 @@ public void Dispose() SuppressFinalize(this); } - private async Task ReceiveMessagesAsync() + private void ReceiveMessages() { - var parser = new Parser(this); + var parser = new Parser(); parser.MessageReceived += (_, e) => OnMessage(e); // First receival (LoginResponse) @@ -105,10 +106,10 @@ private async Task ReceiveMessagesAsync() throw new InvalidOperationException($"Protocol version {version} unsupported"); int size = ReadVarint32(); - Debug.WriteLine($"Got message size: {size}bytes"); + Debug.WriteLine($"Got message size: {size} bytes"); byte[] payload = Read(size); - Debug.WriteLine($"Successfully read {payload.Length}bytes"); + Debug.WriteLine($"Successfully read: {payload.Length} bytes"); Type type = Parser.BuildProtobufFromTag(((McsProtoTag)tag)); Debug.WriteLine($"RECEIVED PROTO OF TYPE {type.Name}"); @@ -123,7 +124,7 @@ private async Task ReceiveMessagesAsync() Debug.WriteLine("Starting receiver loop."); while (true) { - tag = _sslStream.ReadByte(); + tag = _sslStream!.ReadByte(); size = ReadVarint32(); payload = Read(size); type = Parser.BuildProtobufFromTag((McsProtoTag)tag); @@ -139,7 +140,7 @@ internal byte[] Read(int size) int bytesRead = 0; while (bytesRead < size) { - bytesRead += _sslStream.Read(buffer, bytesRead, size - bytesRead); + bytesRead += _sslStream!.Read(buffer, bytesRead, size - bytesRead); } return buffer; } @@ -150,41 +151,37 @@ internal int ReadVarint32() int shift = 0; while (true) { - byte b = (byte)_sslStream.ReadByte(); + byte b = (byte)_sslStream!.ReadByte(); result |= (b & 0x7F) << shift; - if ((b & 0x80) == 0) - break; + if ((b & 0x80) == 0) break; shift += 7; } return result; } - internal byte[] EncodeVarint32(int value) + internal static byte[] EncodeVarint32(int value) { - List result = new List(); + List result = []; while (value != 0) { byte b = (byte)(value & 0x7F); value >>= 7; - if (value != 0) - b |= 0x80; + if (value != 0) b |= 0x80; result.Add(b); } - return result.ToArray(); + return [.. result]; } - internal void SendPacket(object packet) + private void SendPacket(object packet) { var tagEnum = Parser.GetTagFromProtobufType(packet.GetType()); var header = new byte[] { KMcsVersion, (byte)(int)tagEnum }; - using (var ms = new MemoryStream()) - { - Serializer.Serialize(ms, packet); + using var ms = new MemoryStream(); + Serializer.Serialize(ms, packet); - byte[] payload = ms.ToArray(); - _sslStream.Write([.. header, .. EncodeVarint32(payload.Length), .. payload]); - } + byte[] payload = ms.ToArray(); + _sslStream!.Write([.. header, .. EncodeVarint32(payload.Length), .. payload]); } private void HandlePing(HeartbeatPing? ping) @@ -202,7 +199,7 @@ private void HandlePing(HeartbeatPing? ping) SendPacket(pingResponse); } - internal void Reset(bool noWait = false) + private void Reset(bool noWait = false) { if (!noWait) { @@ -218,28 +215,21 @@ internal void Reset(bool noWait = false) Thread.Sleep(waitTime); } } - _lastReset = DateTime.Now; + Debug.WriteLine("Resetting listener."); + Dispose(); - _tcpClient.Close(); - _sslStream.Close(); - Disconnected?.Invoke(this, EventArgs.Empty); ConnectAsync().GetAwaiter().GetResult(); } - public void Reset() - { - Reset(true); - } - private void StatusCheck(object? state = null) { TimeSpan timeSinceLastMessage = DateTime.UtcNow - _timeLastMessageReceived; if (timeSinceLastMessage > TimeSpan.FromSeconds(MaxSilentIntervalSecs)) { Debug.WriteLine($"No communications received in {timeSinceLastMessage.TotalSeconds}s. Resetting connection."); - Reset(); + Reset(true); } else { @@ -265,7 +255,7 @@ private void OnMessage(MessageEventArgs e) HandlePing(e.Object as HeartbeatPing); break; case McsProtoTag.KCloseTag: - Reset(); + Reset(true); break; case McsProtoTag.KIqStanzaTag: break; // I'm not sure about what this message does, and it arrives partially empty, so I will just leave it like this for now @@ -290,12 +280,18 @@ private void OnDataMessage(DataMessageStanza? dataMessage) ex.Message.Contains("salt is missing")) { Debug.WriteLine($"Message dropped as it could not be decrypted: {ex.Message}"); - persistentIds?.Add(dataMessage!.PersistentId); return; } } - persistentIds?.Add(dataMessage!.PersistentId); - NotificationReceived?.Invoke(this, message); + finally + { + persistentIds?.Add(dataMessage!.PersistentId); + } + + var rustPlusMessage = JsonConvert.DeserializeObject(message); + var formattedMessage = JsonConvert.SerializeObject(rustPlusMessage, Formatting.Indented); + + NotificationReceived?.Invoke(this, formattedMessage); } } } diff --git a/RustPlusApi/RustPlusApi.Fcm/RustPlusApi.Fcm.csproj b/RustPlusApi/RustPlusApi.Fcm/RustPlusApi.Fcm.csproj index 796e396..61fd5ea 100644 --- a/RustPlusApi/RustPlusApi.Fcm/RustPlusApi.Fcm.csproj +++ b/RustPlusApi/RustPlusApi.Fcm/RustPlusApi.Fcm.csproj @@ -7,7 +7,7 @@ - + diff --git a/RustPlusApi/RustPlusApi.Fcm/Tools/GcmTools.cs b/RustPlusApi/RustPlusApi.Fcm/Tools/GcmTools.cs index ef4718f..b3ff763 100644 --- a/RustPlusApi/RustPlusApi.Fcm/Tools/GcmTools.cs +++ b/RustPlusApi/RustPlusApi.Fcm/Tools/GcmTools.cs @@ -1,8 +1,10 @@ using System.Diagnostics; using System.Net.Http.Headers; + using AndroidCheckinProto; + using CheckinProto; -using McsProto; + using ProtoBuf; using RustPlusApi.Fcm.Data; @@ -34,25 +36,23 @@ public static async Task CheckInAsync(ulong? androidId = var request = new HttpRequestMessage(HttpMethod.Post, CheckInUrl); - using (var ms = new MemoryStream()) - { - Serializer.Serialize(ms, requestBody); + using var ms = new MemoryStream(); + Serializer.Serialize(ms, requestBody); - var content = new ByteArrayContent(ms.ToArray()); - content.Headers.ContentType = new MediaTypeHeaderValue("application/x-protobuf"); + var content = new ByteArrayContent(ms.ToArray()); + content.Headers.ContentType = new MediaTypeHeaderValue("application/x-protobuf"); - request.Content = content; + request.Content = content; - var response = await HttpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); + var response = await HttpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); - var data = await response.Content.ReadAsByteArrayAsync(); + var data = await response.Content.ReadAsByteArrayAsync(); - using var stream = new MemoryStream(data); - var message = Serializer.Deserialize(stream); + using var stream = new MemoryStream(data); + var message = Serializer.Deserialize(stream); - return message; - } + return message; } catch (Exception ex) { diff --git a/RustPlusApi/RustPlusApi.Fcm/Utils/DecryptionUtility.cs b/RustPlusApi/RustPlusApi.Fcm/Utils/DecryptionUtility.cs index 648752d..ac66c7b 100644 --- a/RustPlusApi/RustPlusApi.Fcm/Utils/DecryptionUtility.cs +++ b/RustPlusApi/RustPlusApi.Fcm/Utils/DecryptionUtility.cs @@ -1,7 +1,9 @@ using System.Diagnostics; using System.Security.Cryptography; using System.Text; + using McsProto; + using Org.BouncyCastle.Asn1.Nist; using Org.BouncyCastle.Asn1.X9; using Org.BouncyCastle.Crypto; @@ -12,388 +14,378 @@ using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Math; using Org.BouncyCastle.Security; + using RustPlusApi.Fcm.Data; + using ECCurve = Org.BouncyCastle.Math.EC.ECCurve; namespace RustPlusApi.Fcm.Utils { - internal class DecryptionUtility - { - private const int NonceBitSize = 128; - private const int MacBitSize = 128; - private const int SHA_256_LENGTH = 32; - private const int KEY_LENGTH = 16; - private const int NONCE_LENGTH = 12; - private const int HEADER_RS = 4096; - private const int TAG_LENGTH = 16; - private const int CHUNK_SIZE = HEADER_RS + TAG_LENGTH; - private const int PADSIZE = 2; - private static readonly ECDomainParameters ecDomainParameters; - private static readonly ECKeyGenerationParameters eckgparameters; - private static readonly ECCurve ecCurve; - private static readonly ECDomainParameters ecSpec; - private static readonly X9ECParameters ecParams = NistNamedCurves.GetByName("P-256"); - private static readonly SecureRandom secureRandom = new(); - - // CEK_INFO = "Content-Encoding: aesgcm" || 0x00 - private static readonly byte[] keyInfoParameter = Encoding.ASCII.GetBytes("Content-Encoding: aesgcm\0"); - - // NONCE_INFO = "Content-Encoding: nonce" || 0x00 - private static readonly byte[] _nonceInfoParameter = Encoding.ASCII.GetBytes("Content-Encoding: nonce\0"); - private static readonly byte[] _authInfoParameter = Encoding.ASCII.GetBytes("Content-Encoding: auth\0"); - private static readonly byte[] _keyLabel = Encoding.ASCII.GetBytes("P-256"); - - private readonly ECPrivateKeyParameters _privateKey; - private readonly ECPublicKeyParameters _publicKey; - - static DecryptionUtility() - { - ecCurve = ecParams.Curve; - ecSpec = new ECDomainParameters(ecCurve, ecParams.G, ecParams.N, ecParams.H, ecParams.GetSeed()); - - eckgparameters = new ECKeyGenerationParameters(ecSpec, secureRandom); - ecDomainParameters = eckgparameters.DomainParameters; - } - - public DecryptionUtility() - { - (_privateKey, _publicKey) = GenerateKeys(); - PublicKey = _publicKey.Q.GetEncoded(); - - AuthSecret = new byte[16]; - secureRandom.NextBytes(AuthSecret); - } - - internal DecryptionUtility(byte[] publicKey, byte[] privateKey, byte[] authSecret) - { - var pt = ecCurve.DecodePoint(publicKey); + internal class DecryptionUtility + { + private const int SHA_256_LENGTH = 32; + private const int KEY_LENGTH = 16; + private const int NONCE_LENGTH = 12; + private const int HEADER_RS = 4096; + private const int TAG_LENGTH = 16; + private const int CHUNK_SIZE = HEADER_RS + TAG_LENGTH; + private const int PADSIZE = 2; + + private static readonly ECDomainParameters ecDomainParameters; + private static readonly ECKeyGenerationParameters eckgparameters; + private static readonly ECCurve ecCurve; + private static readonly ECDomainParameters ecSpec; + private static readonly X9ECParameters ecParams = NistNamedCurves.GetByName("P-256"); + private static readonly SecureRandom secureRandom = new(); + + private static readonly byte[] keyInfoParameter = Encoding.ASCII.GetBytes("Content-Encoding: aesgcm\0"); + private static readonly byte[] _nonceInfoParameter = Encoding.ASCII.GetBytes("Content-Encoding: nonce\0"); + private static readonly byte[] _authInfoParameter = Encoding.ASCII.GetBytes("Content-Encoding: auth\0"); + private static readonly byte[] _keyLabel = Encoding.ASCII.GetBytes("P-256"); + + private readonly ECPrivateKeyParameters _privateKey; + private readonly ECPublicKeyParameters _publicKey; + + private byte[] AuthSecret { get; } + private byte[] PublicKey { get; } + + static DecryptionUtility() + { + ecCurve = ecParams.Curve; + ecSpec = new ECDomainParameters(ecCurve, ecParams.G, ecParams.N, ecParams.H, ecParams.GetSeed()); + + eckgparameters = new ECKeyGenerationParameters(ecSpec, secureRandom); + ecDomainParameters = eckgparameters.DomainParameters; + } + + public DecryptionUtility() + { + (_privateKey, _publicKey) = GenerateKeys(); + PublicKey = _publicKey.Q.GetEncoded(); + + AuthSecret = new byte[16]; + secureRandom.NextBytes(AuthSecret); + } + + public static string Decrypt(DataMessageStanza dataMessage, Keys keys) + { + var decryptor = new DecryptionUtility(DecodeBase64(keys.PublicKey), DecodeBase64(keys.PrivateKey), DecodeBase64(keys.AuthSecret)); + + var cryptoKey = dataMessage.AppDatas.FirstOrDefault(item => item.Key == "crypto-key") + ?? throw new Exception("crypto-key is missing"); + + var salt = dataMessage.AppDatas.FirstOrDefault(item => item.Key == "encryption") + ?? throw new Exception("salt is missing"); + + var decryptedBytes = decryptor.Decrypt(dataMessage.RawData, DecodeBase64(cryptoKey.Value[3..]), DecodeBase64(salt.Value[5..])); + + return Encoding.UTF8.GetString(decryptedBytes); + } + + private byte[] Decrypt(byte[] buffer, byte[] senderPublicKeyBytes, byte[] salt) + { + var ecP = NistNamedCurves.GetByName("P-256"); + var eCDomainParameters = new ECDomainParameters(ecP.Curve, ecP.G, ecP.N); + + var pt = ecP.Curve.DecodePoint(senderPublicKeyBytes); + var senderPublicKey = new ECPublicKeyParameters(pt, eCDomainParameters); + + var (key, nonce) = DeriveKeyAndNonce(salt, AuthSecret, senderPublicKey, _publicKey, _privateKey); + + var result = Array.Empty(); + var start = 0; + + // TODO: this is not tested with more than one iteration + for (uint i = 0; start < buffer.Length; ++i) + { + var end = start + CHUNK_SIZE; + if (end == buffer.Length) throw new InvalidOperationException("Truncated payload"); + + end = Math.Min(end, buffer.Length); + + if (end - start <= TAG_LENGTH) throw new InvalidOperationException("Invalid block: too small at " + i); + + var block = DecryptRecord(key, nonce, i, ByteArray.Slice(buffer, start, end), end >= buffer.Length); + result = ByteArray.Concat(result, block); + start = end; + } + return result; + } + + private DecryptionUtility(byte[] publicKey, byte[] privateKey, byte[] authSecret) + { + var pt = ecCurve.DecodePoint(publicKey); _publicKey = new ECPublicKeyParameters(pt, ecDomainParameters); _privateKey = new ECPrivateKeyParameters(new BigInteger(1, privateKey), ecDomainParameters); - AuthSecret = authSecret; - PublicKey = _publicKey.Q.GetEncoded(); - } - - public byte[] AuthSecret { get; } - public byte[] PublicKey { get; } - - private byte[] AddLengthPrefix(byte[] buffer) - { - var newBuffer = new byte[buffer.Length + 2]; - Array.Copy(buffer, 0, newBuffer, 2, buffer.Length); - - var intBytes = BitConverter.GetBytes((short)buffer.Length); - - if (BitConverter.IsLittleEndian) Array.Reverse(intBytes); - - Debug.Assert(intBytes.Length <= 2); - Array.Copy(intBytes, 0, newBuffer, 0, intBytes.Length); - - return newBuffer; - } - - public byte[] Decrypt(byte[] buffer, byte[] senderPublicKeyBytes, byte[] salt) - { - var ecP = NistNamedCurves.GetByName("P-256"); - var eCDomainParameters = new ECDomainParameters(ecP.Curve, ecP.G, ecP.N); - - var pt = ecP.Curve.DecodePoint(senderPublicKeyBytes); - var senderPublicKey = new ECPublicKeyParameters(pt, eCDomainParameters); - - var (key, nonce) = DeriveKeyAndNonce(salt, AuthSecret, senderPublicKey, _publicKey, _privateKey); - - var result = new byte[0]; - var start = 0; - - // TODO: this is not tested with more than one iteration - for (uint i = 0; start < buffer.Length; ++i) - { - var end = start + CHUNK_SIZE; - if (end == buffer.Length) throw new InvalidOperationException("Truncated payload"); - - end = Math.Min(end, buffer.Length); + AuthSecret = authSecret; + PublicKey = _publicKey.Q.GetEncoded(); + } - if (end - start <= TAG_LENGTH) throw new InvalidOperationException("Invalid block: too small at " + i); + private static byte[] AddLengthPrefix(byte[] buffer) + { + var newBuffer = new byte[buffer.Length + 2]; + Array.Copy(buffer, 0, newBuffer, 2, buffer.Length); - var block = DecryptRecord(key, nonce, i, ByteArray.Slice(buffer, start, end), end >= buffer.Length); - result = ByteArray.Concat(result, block); - start = end; - } + var intBytes = BitConverter.GetBytes((short)buffer.Length); - return result; - } + if (BitConverter.IsLittleEndian) Array.Reverse(intBytes); - private byte[] RemovePad(byte[] buffer) - { - var pad = (int)ByteArray.ReadUInt64(buffer, 0, PADSIZE); + Debug.Assert(intBytes.Length <= 2); + Array.Copy(intBytes, 0, newBuffer, 0, intBytes.Length); - if (pad + PADSIZE > buffer.Length) throw new InvalidOperationException("padding exceeds block size"); + return newBuffer; + } - return ByteArray.Slice(buffer, pad + PADSIZE, buffer.Length); - } + private static byte[] RemovePad(byte[] buffer) + { + var pad = (int)ByteArray.ReadUInt64(buffer, 0, PADSIZE); - private byte[] DecryptRecord(byte[] key, byte[] nonce, uint counter, byte[] buffer, bool last) - { - nonce = GenerateNonce(nonce, counter); + if (pad + PADSIZE > buffer.Length) throw new InvalidOperationException("padding exceeds block size"); - var blockCipher = new GcmBlockCipher(new AesEngine()); + return ByteArray.Slice(buffer, pad + PADSIZE, buffer.Length); + } - blockCipher.Init(false, new AeadParameters(new KeyParameter(key), 128, nonce)); + private static byte[] DecryptRecord(byte[] key, byte[] nonce, uint counter, byte[] buffer, bool last) + { + nonce = GenerateNonce(nonce, counter); - var decryptedMessage = new byte[blockCipher.GetOutputSize(buffer.Length)]; + var blockCipher = new GcmBlockCipher(new AesEngine()); - var decryptedMessageLength = blockCipher.ProcessBytes(buffer, 0, buffer.Length, decryptedMessage, 0); + blockCipher.Init(false, new AeadParameters(new KeyParameter(key), 128, nonce)); - decryptedMessageLength += blockCipher.DoFinal(decryptedMessage, decryptedMessageLength); + var decryptedMessage = new byte[blockCipher.GetOutputSize(buffer.Length)]; - return RemovePad(decryptedMessage); - } + var decryptedMessageLength = blockCipher.ProcessBytes(buffer, 0, buffer.Length, decryptedMessage, 0); - private (byte[], byte[]) DeriveKeyAndNonce(byte[] salt, byte[] authSecret, ECPublicKeyParameters senderPublicKey, - ECPublicKeyParameters receiverPublicKey, ECPrivateKeyParameters receiverPrivateKey) - { - var (secret, context) = ExtractSecretAndContext(senderPublicKey, receiverPublicKey, receiverPrivateKey); - secret = HKDF.GetBytes(authSecret, secret, _authInfoParameter, SHA_256_LENGTH); + decryptedMessageLength += blockCipher.DoFinal(decryptedMessage, decryptedMessageLength); - var keyInfo = ByteArray.Concat(keyInfoParameter, context); - var nonceInfo = ByteArray.Concat(_nonceInfoParameter, context); + return RemovePad(decryptedMessage); + } - var prk = HKDF.Extract(salt, secret); + private static (byte[], byte[]) DeriveKeyAndNonce(byte[] salt, byte[] authSecret, ECPublicKeyParameters senderPublicKey, + ECPublicKeyParameters receiverPublicKey, ECPrivateKeyParameters receiverPrivateKey) + { + var (secret, context) = ExtractSecretAndContext(senderPublicKey, receiverPublicKey, receiverPrivateKey); + secret = HKDF.GetBytes(authSecret, secret, _authInfoParameter, SHA_256_LENGTH); - return (HKDF.Expand(prk, keyInfo, KEY_LENGTH), HKDF.Expand(prk, nonceInfo, NONCE_LENGTH)); - } + var keyInfo = ByteArray.Concat(keyInfoParameter, context); + var nonceInfo = ByteArray.Concat(_nonceInfoParameter, context); - private (byte[], byte[]) ExtractSecretAndContext(ECPublicKeyParameters senderPublicKey, - ECPublicKeyParameters receiverPublicKey, ECPrivateKeyParameters receiverPrivateKey) - { - IBasicAgreement aKeyAgree = new ECDHBasicAgreement(); + var prk = HKDF.Extract(salt, secret); - aKeyAgree.Init(receiverPrivateKey); - var sharedSecret = aKeyAgree.CalculateAgreement(senderPublicKey).ToByteArrayUnsigned(); + return (HKDF.Expand(prk, keyInfo, KEY_LENGTH), HKDF.Expand(prk, nonceInfo, NONCE_LENGTH)); + } - var receiverKeyBytes = AddLengthPrefix(receiverPublicKey.Q.GetEncoded()); - var senderPublicKeyBytes = AddLengthPrefix(senderPublicKey.Q.GetEncoded()); + private static (byte[], byte[]) ExtractSecretAndContext(ECPublicKeyParameters senderPublicKey, + ECPublicKeyParameters receiverPublicKey, ECPrivateKeyParameters receiverPrivateKey) + { + IBasicAgreement aKeyAgree = new ECDHBasicAgreement(); - var context = new byte[_keyLabel.Length + 1 + receiverKeyBytes.Length + senderPublicKeyBytes.Length]; + aKeyAgree.Init(receiverPrivateKey); + var sharedSecret = aKeyAgree.CalculateAgreement(senderPublicKey).ToByteArrayUnsigned(); - var destinationOffset = 0; - Array.Copy(_keyLabel, 0, context, destinationOffset, _keyLabel.Length); - destinationOffset += _keyLabel.Length + 1; - Array.Copy(receiverKeyBytes, 0, context, destinationOffset, receiverKeyBytes.Length); - destinationOffset += receiverKeyBytes.Length; - Array.Copy(senderPublicKeyBytes, 0, context, destinationOffset, senderPublicKeyBytes.Length); + var receiverKeyBytes = AddLengthPrefix(receiverPublicKey.Q.GetEncoded()); + var senderPublicKeyBytes = AddLengthPrefix(senderPublicKey.Q.GetEncoded()); - return (sharedSecret, context); - } - - private byte[] GenerateNonce(byte[] buffer, uint counter) - { - var nonce = new byte[buffer.Length]; - Buffer.BlockCopy(buffer, 0, nonce, 0, buffer.Length); - var m = ByteArray.ReadUInt64(nonce, nonce.Length - 6, 6); - var x = ((m ^ counter) & 0xffffff) + (((m / 0x1000000) ^ (counter / 0x1000000)) & 0xffffff) * 0x1000000; - ByteArray.WriteUInt64(nonce, m, nonce.Length - 6, 6); - - return nonce; - } - - private (ECPrivateKeyParameters, ECPublicKeyParameters) GenerateKeys() - { - var gen = new ECKeyPairGenerator("ECDH"); - gen.Init(eckgparameters); - var eckp = gen.GenerateKeyPair(); - - var ecPub = (ECPublicKeyParameters)eckp.Public; - var ecPri = (ECPrivateKeyParameters)eckp.Private; - - return (ecPri, ecPub); - } - - private static byte[] DecodeBase64(string base64) - { - base64 = base64.Replace('-', '+').Replace('_', '/'); - - while (base64.Length % 4 != 0) base64 += "="; - - return Convert.FromBase64String(base64); - } - - public static string Decrypt(DataMessageStanza dataMessage, Keys keys) - { - var decryptor = new DecryptionUtility(DecodeBase64(keys.PublicKey), DecodeBase64(keys.PrivateKey), - DecodeBase64(keys.AuthSecret)); - var cryptoKey = dataMessage.AppDatas.FirstOrDefault(item => item.Key == "crypto-key") ?? - throw new Exception("crypto-key is missing"); - var salt = dataMessage.AppDatas.FirstOrDefault(item => item.Key == "encryption") ?? - throw new Exception("salt is missing"); - var decryptedBytes = decryptor.Decrypt(dataMessage.RawData, DecodeBase64(cryptoKey.Value[3..]), - DecodeBase64(salt.Value[5..])); - - return Encoding.UTF8.GetString(decryptedBytes); - } - - private static class ByteArray - { - // TODO: optimize this method to use pointers or bit shifts - // check if it is working with big endian - public static ulong ReadUInt64(byte[] bytes, int startIndex, int length) - { - var buffer = new byte[8]; - Buffer.BlockCopy(bytes, startIndex, buffer, 8 - length, length); - - if (BitConverter.IsLittleEndian) Array.Reverse(buffer); - - return BitConverter.ToUInt64(buffer, 0); - } - - // TODO: check if it is working with big endian - public static void WriteUInt64(byte[] source, ulong value, int startIndex, int length) - { - var buffer = BitConverter.GetBytes(value); - Buffer.BlockCopy(source, startIndex, buffer, 0, length); - } - - public static byte[] Slice(byte[] source, int startIndex, int endIndex) - { - Debug.Assert(startIndex < endIndex); - - var length = endIndex - startIndex; - var result = new byte[length]; - Buffer.BlockCopy(source, startIndex, result, 0, length); - return result; - } - - public static byte[] Concat(byte[] first, byte[] second) - { - var ret = new byte[first.Length + second.Length]; - Buffer.BlockCopy(first, 0, ret, 0, first.Length); - Buffer.BlockCopy(second, 0, ret, first.Length, second.Length); - return ret; - } - } - - private class HKDF - { - /// - /// Returns a 32 byte psuedorandom number that can be used with the Expand method if - /// a cryptographically secure pseudorandom number is not already available. - /// - /// - /// (Optional, but you should use it) Non-secret random value. - /// If less than 64 bytes it is padded with zeros. Can be reused but output is - /// stronger if not reused. (And of course output is much stronger with salt than - /// without it) - /// - /// - /// Material that is not necessarily random that - /// will be used with the HMACSHA256 hash function and the salt to produce - /// a 32 byte psuedorandom number. - /// - /// - public static byte[] Extract(byte[] salt, byte[] inputKeyMaterial) - { - //For algorithm docs, see section 2.2: https://tools.ietf.org/html/rfc5869 - - using (var hmac = new HMACSHA256(salt)) - { - return hmac.ComputeHash(inputKeyMaterial, 0, inputKeyMaterial.Length); - } - } - - - /// - /// Returns a secure pseudorandom key of the desired length. Useful as a key derivation - /// function to derive one cryptograpically secure pseudorandom key from another - /// cryptograpically secure pseudorandom key. This can be useful, for example, - /// when needing to create a subKey from a master key. - /// - /// - /// A cryptograpically secure pseudorandom number. Can be obtained - /// via the Extract method or elsewhere. Must be 32 bytes or greater. 64 bytes is - /// the prefered size. Shorter keys are padded to 64 bytes, longer ones are hashed - /// to 64 bytes. - /// - /// - /// (Optional) Context and application specific information. - /// Allows the output to be bound to application context related information. - /// - /// Length of output in bytes. - /// - public static byte[] Expand(byte[] key, byte[] info, int length) - { - //For algorithm docs, see section 2.3: https://tools.ietf.org/html/rfc5869 - //Also note: - // SHA256 has a block size of 64 bytes - // SHA256 has an output length of 32 bytes (but can be truncated to less) - - const int hashLength = 32; - - //Min recommended length for Key is the size of the hash output (32 bytes in this case) - //See section 2: https://tools.ietf.org/html/rfc2104#section-3 - //Also see: http://security.stackexchange.com/questions/95972/what-are-requirements-for-hmac-secret-key - if (key == null || key.Length < 32) - throw new ArgumentOutOfRangeException("Key should be 32 bytes or greater."); - - if (length > 255 * hashLength) - throw new ArgumentOutOfRangeException( - "Output length must 8160 bytes or less which is 255 * the SHA256 block site of 32 bytes."); - - var outputIndex = 0; - byte[] buffer; - var hash = new byte[0]; - var output = new byte[length]; - var count = 1; - int bytesToCopy; - - using (var hmac = new HMACSHA256(key)) - { - while (outputIndex < length) - { - //Setup buffer to hash - buffer = new byte[hash.Length + info.Length + 1]; - Buffer.BlockCopy(hash, 0, buffer, 0, hash.Length); - Buffer.BlockCopy(info, 0, buffer, hash.Length, info.Length); - buffer[buffer.Length - 1] = (byte)count++; - - //Hash the buffer and return a 32 byte hash - hash = hmac.ComputeHash(buffer, 0, buffer.Length); - - //Copy as much of the hash as we need to the final output - bytesToCopy = Math.Min(length - outputIndex, hash.Length); - Buffer.BlockCopy(hash, 0, output, outputIndex, bytesToCopy); - outputIndex += bytesToCopy; - } - } - - return output; - } - - - /// - /// Generates a psuedorandom number of the length specified. This number is suitable - /// for use as an encryption key, HMAC validation key or other uses of a cryptographically - /// secure psuedorandom number. - /// - /// - /// non-secret random value. If less than 64 bytes it is - /// padded with zeros. Can be reused but output is stronger if not reused. - /// - /// - /// Material that is not necessarily random that - /// will be used with the HMACSHA256 hash function and the salt to produce - /// a 32 byte psuedorandom number. - /// - /// - /// (Optional) context and application specific information. - /// Allows the output to be bound to application context related information. Pass 0 length - /// byte array to omit. - /// - /// Length of output in bytes. - public static byte[] GetBytes(byte[] salt, byte[] inputKeyMaterial, byte[] info, int length) - { - var key = Extract(salt, inputKeyMaterial); - return Expand(key, info, length); - } - } - } + var context = new byte[_keyLabel.Length + 1 + receiverKeyBytes.Length + senderPublicKeyBytes.Length]; + + var destinationOffset = 0; + Array.Copy(_keyLabel, 0, context, destinationOffset, _keyLabel.Length); + destinationOffset += _keyLabel.Length + 1; + Array.Copy(receiverKeyBytes, 0, context, destinationOffset, receiverKeyBytes.Length); + destinationOffset += receiverKeyBytes.Length; + Array.Copy(senderPublicKeyBytes, 0, context, destinationOffset, senderPublicKeyBytes.Length); + + return (sharedSecret, context); + } + + private static byte[] GenerateNonce(byte[] buffer, uint counter) + { + var nonce = new byte[buffer.Length]; + Buffer.BlockCopy(buffer, 0, nonce, 0, buffer.Length); + var m = ByteArray.ReadUInt64(nonce, nonce.Length - 6, 6); + //var x = ((m ^ counter) & 0xffffff) + (((m / 0x1000000) ^ (counter / 0x1000000)) & 0xffffff) * 0x1000000; + ByteArray.WriteUInt64(nonce, m, nonce.Length - 6, 6); + + return nonce; + } + + private static (ECPrivateKeyParameters, ECPublicKeyParameters) GenerateKeys() + { + var gen = new ECKeyPairGenerator("ECDH"); + gen.Init(eckgparameters); + var eckp = gen.GenerateKeyPair(); + + var ecPub = (ECPublicKeyParameters)eckp.Public; + var ecPri = (ECPrivateKeyParameters)eckp.Private; + + return (ecPri, ecPub); + } + + private static byte[] DecodeBase64(string base64) + { + base64 = base64.Replace('-', '+').Replace('_', '/'); + + while (base64.Length % 4 != 0) base64 += "="; + + return Convert.FromBase64String(base64); + } + + private static class ByteArray + { + // TODO: optimize this method to use pointers or bit shifts + // check if it is working with big endian + internal static ulong ReadUInt64(byte[] bytes, int startIndex, int length) + { + var buffer = new byte[8]; + Buffer.BlockCopy(bytes, startIndex, buffer, 8 - length, length); + + if (BitConverter.IsLittleEndian) Array.Reverse(buffer); + + return BitConverter.ToUInt64(buffer, 0); + } + + // TODO: check if it is working with big endian + internal static void WriteUInt64(byte[] source, ulong value, int startIndex, int length) + { + var buffer = BitConverter.GetBytes(value); + Buffer.BlockCopy(source, startIndex, buffer, 0, length); + } + + internal static byte[] Slice(byte[] source, int startIndex, int endIndex) + { + Debug.Assert(startIndex < endIndex); + + var length = endIndex - startIndex; + var result = new byte[length]; + Buffer.BlockCopy(source, startIndex, result, 0, length); + return result; + } + + internal static byte[] Concat(byte[] first, byte[] second) + { + var ret = new byte[first.Length + second.Length]; + Buffer.BlockCopy(first, 0, ret, 0, first.Length); + Buffer.BlockCopy(second, 0, ret, first.Length, second.Length); + return ret; + } + } + + private class HKDF + { + /// + /// Returns a 32 byte psuedorandom number that can be used with the Expand method if + /// a cryptographically secure pseudorandom number is not already available. + /// + /// + /// (Optional, but you should use it) Non-secret random value. + /// If less than 64 bytes it is padded with zeros. Can be reused but output is + /// stronger if not reused. (And of course output is much stronger with salt than + /// without it) + /// + /// + /// Material that is not necessarily random that + /// will be used with the HMACSHA256 hash function and the salt to produce + /// a 32 byte psuedorandom number. + /// + /// + internal static byte[] Extract(byte[] salt, byte[] inputKeyMaterial) + { + //For algorithm docs, see section 2.2: https://tools.ietf.org/html/rfc5869 + + using var hmac = new HMACSHA256(salt); + return hmac.ComputeHash(inputKeyMaterial, 0, inputKeyMaterial.Length); + } + + /// + /// Returns a secure pseudorandom key of the desired length. Useful as a key derivation + /// function to derive one cryptograpically secure pseudorandom key from another + /// cryptograpically secure pseudorandom key. This can be useful, for example, + /// when needing to create a subKey from a master key. + /// + /// + /// A cryptograpically secure pseudorandom number. Can be obtained + /// via the Extract method or elsewhere. Must be 32 bytes or greater. 64 bytes is + /// the prefered size. Shorter keys are padded to 64 bytes, longer ones are hashed + /// to 64 bytes. + /// + /// + /// (Optional) Context and application specific information. + /// Allows the output to be bound to application context related information. + /// + /// Length of output in bytes. + /// + internal static byte[] Expand(byte[] key, byte[] info, int length) + { + //For algorithm docs, see section 2.3: https://tools.ietf.org/html/rfc5869 + //Also note: + // SHA256 has a block size of 64 bytes + // SHA256 has an output length of 32 bytes (but can be truncated to less) + const int hashLength = 32; + + //Min recommended length for Key is the size of the hash output (32 bytes in this case) + //See section 2: https://tools.ietf.org/html/rfc2104#section-3 + //Also see: http://security.stackexchange.com/questions/95972/what-are-requirements-for-hmac-secret-key + if (key == null || key.Length < 32) + throw new ArgumentOutOfRangeException("Key should be 32 bytes or greater."); + + ArgumentOutOfRangeException.ThrowIfGreaterThan(length, 255 * hashLength); + + var outputIndex = 0; + byte[] buffer; + var hash = Array.Empty(); + var output = new byte[length]; + var count = 1; + int bytesToCopy; + + using (var hmac = new HMACSHA256(key)) + { + while (outputIndex < length) + { + //Setup buffer to hash + buffer = new byte[hash.Length + info.Length + 1]; + Buffer.BlockCopy(hash, 0, buffer, 0, hash.Length); + Buffer.BlockCopy(info, 0, buffer, hash.Length, info.Length); + buffer[^1] = (byte)count++; + + //Hash the buffer and return a 32 byte hash + hash = hmac.ComputeHash(buffer, 0, buffer.Length); + + //Copy as much of the hash as we need to the final output + bytesToCopy = Math.Min(length - outputIndex, hash.Length); + Buffer.BlockCopy(hash, 0, output, outputIndex, bytesToCopy); + outputIndex += bytesToCopy; + } + } + return output; + } + + /// + /// Generates a psuedorandom number of the length specified. This number is suitable + /// for use as an encryption key, HMAC validation key or other uses of a cryptographically + /// secure psuedorandom number. + /// + /// + /// non-secret random value. If less than 64 bytes it is + /// padded with zeros. Can be reused but output is stronger if not reused. + /// + /// + /// Material that is not necessarily random that + /// will be used with the HMACSHA256 hash function and the salt to produce + /// a 32 byte psuedorandom number. + /// + /// + /// (Optional) context and application specific information. + /// Allows the output to be bound to application context related information. Pass 0 length + /// byte array to omit. + /// + /// Length of output in bytes. + internal static byte[] GetBytes(byte[] salt, byte[] inputKeyMaterial, byte[] info, int length) + { + var key = Extract(salt, inputKeyMaterial); + return Expand(key, info, length); + } + } + } } \ No newline at end of file diff --git a/RustPlusApi/RustPlusApi.Fcm/Utils/MessageEventArg.cs b/RustPlusApi/RustPlusApi.Fcm/Utils/MessageEventArg.cs index 7c42d78..305775a 100644 --- a/RustPlusApi/RustPlusApi.Fcm/Utils/MessageEventArg.cs +++ b/RustPlusApi/RustPlusApi.Fcm/Utils/MessageEventArg.cs @@ -2,7 +2,7 @@ namespace RustPlusApi.Fcm.Utils { - public class MessageEventArgs : EventArgs + internal class MessageEventArgs : EventArgs { public McsProtoTag Tag { get; set; } public object? Object { get; set; } diff --git a/RustPlusApi/RustPlusApi.Fcm/Utils/Parser.cs b/RustPlusApi/RustPlusApi.Fcm/Utils/Parser.cs index da26655..c81e127 100644 --- a/RustPlusApi/RustPlusApi.Fcm/Utils/Parser.cs +++ b/RustPlusApi/RustPlusApi.Fcm/Utils/Parser.cs @@ -8,23 +8,15 @@ namespace RustPlusApi.Fcm.Utils { - public class Parser + internal class Parser() { - public event EventHandler? ErrorOccurred; - public event EventHandler? MessageReceived; + internal event EventHandler? ErrorOccurred; + internal event EventHandler? MessageReceived; - private FcmListener _listener; - private ProcessingState _state = ProcessingState.McsVersionTagAndSize; private byte[] _data = []; - private bool _handshakeComplete; - public Parser(FcmListener listener) - { - _listener = listener; // Link to instance just to be able to reset from parser. Maybe will need this for something else later, or could be done better - } - - public void OnData(byte[] buffer, Type type) + internal void OnData(byte[] buffer, Type type) { _data = buffer; OnGotMessageBytes(type); @@ -37,53 +29,51 @@ internal void OnGotLoginResponse() private void OnGotMessageBytes(Type type) { - var messageTag = GetTagFromProtobufType(type); - - if (_data.Length == 0) + try { - MessageReceived?.Invoke(this, new MessageEventArgs { Tag = messageTag, Object = Activator.CreateInstance(type) }); - return; - } + var messageTag = GetTagFromProtobufType(type); - var buffer = _data.Take(_data.Length).ToArray(); - _data = _data.Skip(_data.Length).ToArray(); + if (_data.Length == 0) + { + MessageReceived?.Invoke(this, new MessageEventArgs { Tag = messageTag, Object = Activator.CreateInstance(type) }); + return; + } - using var stream = new MemoryStream(buffer); - var message = Serializer.NonGeneric.Deserialize(type, stream); + var buffer = _data.Take(_data.Length).ToArray(); + _data = _data.Skip(_data.Length).ToArray(); - MessageReceived?.Invoke(this, new MessageEventArgs { Tag = messageTag, Object = message }); + using var stream = new MemoryStream(buffer); + var message = Serializer.NonGeneric.Deserialize(type, stream); - if (messageTag == McsProtoTag.KLoginResponseTag) - { - if (_handshakeComplete) Debug.WriteLine("Unexpected login response"); - else + MessageReceived?.Invoke(this, new MessageEventArgs { Tag = messageTag, Object = message }); + + if (messageTag == McsProtoTag.KLoginResponseTag) { - _handshakeComplete = true; - Debug.WriteLine("GCM Handshake complete."); + if (_handshakeComplete) Debug.WriteLine("Unexpected login response"); + else + { + _handshakeComplete = true; + Debug.WriteLine("GCM Handshake complete."); + } } } + catch (Exception ex) + { + ErrorOccurred?.Invoke(this, ex); + } } internal static McsProtoTag GetTagFromProtobufType(Type type) { - if (type == typeof(HeartbeatPing)) - return McsProtoTag.KHeartbeatPingTag; - else if (type == typeof(HeartbeatAck)) - return McsProtoTag.KHeartbeatAckTag; - else if (type == typeof(LoginRequest)) - return McsProtoTag.KLoginRequestTag; - else if (type == typeof(LoginResponse)) - return McsProtoTag.KLoginResponseTag; - else if (type == typeof(Close)) - return McsProtoTag.KCloseTag; - else if (type == typeof(IqStanza)) - return McsProtoTag.KIqStanzaTag; - else if (type == typeof(DataMessageStanza)) - return McsProtoTag.KDataMessageStanzaTag; - else if (type == typeof(StreamErrorStanza)) - return McsProtoTag.KStreamErrorStanzaTag; - else - throw new ArgumentOutOfRangeException(nameof(type), type, null); + if (type == typeof(HeartbeatPing)) return McsProtoTag.KHeartbeatPingTag; + else if (type == typeof(HeartbeatAck)) return McsProtoTag.KHeartbeatAckTag; + else if (type == typeof(LoginRequest)) return McsProtoTag.KLoginRequestTag; + else if (type == typeof(LoginResponse)) return McsProtoTag.KLoginResponseTag; + else if (type == typeof(Close)) return McsProtoTag.KCloseTag; + else if (type == typeof(IqStanza)) return McsProtoTag.KIqStanzaTag; + else if (type == typeof(DataMessageStanza)) return McsProtoTag.KDataMessageStanzaTag; + else if (type == typeof(StreamErrorStanza)) return McsProtoTag.KStreamErrorStanzaTag; + else throw new ArgumentOutOfRangeException(nameof(type), type, null); } internal static Type BuildProtobufFromTag(McsProtoTag tag)