using ExtensionMethods; using MAX.Models; using Microsoft.Extensions.Logging; using System; using System.IO; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; namespace MAX { public class Client : IDisposable { private ILogger logger; private string host; private int port; private int vendorId; private string serialNumber; private int userId; private string username; private string password; private TcpClient connection = null; private NetworkStream connectionStream = null; private TripleDES des = null; private bool disposed = false; public Client(ILogger logger, string host, int port, int vendorId, string serialNumber, int userId, string username, string password) { this.logger = logger; this.host = host; this.port = port; this.vendorId = vendorId; this.serialNumber = serialNumber; this.userId = userId; this.username = username; this.password = password; ConnectTimeout = 10000; ReceiveTimeout = 10000; SendTimeout = 10000; } public void Close() { Dispose(true); } public async Task ConnectAsync() { if (connection != null) throw new Exception("Already connected"); connection = new TcpClient(AddressFamily.InterNetwork); connection.ReceiveTimeout = ReceiveTimeout; connection.SendTimeout = SendTimeout; // Connect to the server try { using (var cancellationSource = new CancellationTokenSource(ConnectTimeout)) { await connection.ConnectAsync(host, port).WithCancellation(cancellationSource.Token).ConfigureAwait(false); } } catch (OperationCanceledException) { throw new Exception("Connect timeout"); } connectionStream = connection.GetStream(); // Device authentication await WriteMessageAsync(new MessageBuilder() .Append("Hi ") .Append(serialNumber) .Append("|V") .Append(vendorId) .Append("|123451234512345||||||")).ConfigureAwait(false); var response = await ReadMessageAsync().ConfigureAwait(false); if (!response.StartsWith("Hi ")) { logger.LogError("Device authentication failed: {0}", response); return null; } // Request server RSA key await WriteMessageAsync(new MessageBuilder().Append("PK")).ConfigureAwait(false); response = await ReadMessageAsync().ConfigureAwait(false); // Key exchange des = TripleDES.Create(); des.IV = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; await WriteMessageAsync(new MessageBuilder() .Append("3D ") .Append(EncryptRSA(response, BitConverter.ToString(des.Key).Replace("-", "")))).ConfigureAwait(false); response = await ReadMessageAsync().ConfigureAwait(false); if (!response.StartsWith("OK")) { throw new Exception(String.Format("Key exchange failed: {0}", response)); } // User authentication await WriteMessageAsync(new MessageBuilder() .Append("User ") .Append(Encrypt(new StringBuilder() .Append(userId) .Append("|") .Append(username) .Append("|") .Append(password).ToString()))).ConfigureAwait(false); response = Decrypt(await ReadMessageAsync().ConfigureAwait(false)); if (response.StartsWith("OK")) { var parts = response.Split('|'); var user = new User() { Id = userId, Username = username, FirstName = parts[4], Surname = parts[3], Enabled = bool.Parse(parts[6]), Level = (User.UserLevel)int.Parse(parts[1]), System = int.Parse(parts[2]), LastLogin = DateTime.Parse(parts[5]) }; if (user.Level == User.UserLevel.CustomUser) { user.CanPrintOffline = bool.Parse(parts[7]); user.OfflinePrintValue = decimal.Parse(parts[8]); user.CanPrintOnline = bool.Parse(parts[9]); user.OnlinePrintValue = decimal.Parse(parts[10]); user.CanReprintOffline = bool.Parse(parts[11]); user.OfflineReprintValue = decimal.Parse(parts[12]); user.CanReprintOnline = bool.Parse(parts[13]); user.OnlineReprintValue = decimal.Parse(parts[14]); } // Account information await WriteMessageAsync(new MessageBuilder().Append("Acc")).ConfigureAwait(false); response = Decrypt(await ReadMessageAsync().ConfigureAwait(false)); if (response.StartsWith("OK")) { parts = response.Split('|'); user.Account = new Account() { Id = int.Parse(parts[1]), Name = parts[2], Balance = decimal.Parse(parts[3]), Status = (Account.AccountStatus)int.Parse(parts[4]), Reference = parts[5], Warehouse = new Warehouse() { Id = int.Parse(parts[6]), Name = parts[7] } }; return user; } else if (response.StartsWith("ER")) { logger.LogError("Error retrieving account information: {0}", response); return null; } else { throw new Exception(String.Format("Invalid account information response: {0}", response)); } } else if (response.StartsWith("ER")) { logger.LogInformation("User authentication failed: {0}", response); return null; } else { throw new Exception(String.Format("Invalid user information response: {0}", response)); } } public int ConnectTimeout { get; set; } protected virtual void Dispose(bool disposing) { if (disposed) return; disposed = true; // No unmanaged resources are disposed so we don't need the full finalisation pattern. if (disposing) { if (des != null) { des.Dispose(); des = null; } if (connectionStream != null) { connectionStream.Dispose(); connectionStream = null; } if (connection != null) { connection.Dispose(); connection = null; } } } public void Dispose() { Dispose(true); } private string Decrypt(string cipherText) { using (var decryptor = des.CreateDecryptor(des.Key, des.IV)) { return Encoding.UTF8.GetString(Transform(decryptor, Convert.FromBase64String(cipherText))); } } private string Encrypt(string plainText) { using (var encryptor = des.CreateEncryptor(des.Key, des.IV)) { return Convert.ToBase64String(Transform(encryptor, Encoding.UTF8.GetBytes(plainText))); } } private string EncryptRSA(string publicKey, string plainText) { RSAParameters parameters = new RSAParameters(); var xml = new XmlDocument(); xml.LoadXml(publicKey); if (! xml.DocumentElement.Name.Equals("RSAKeyValue")) throw new Exception("Invalid RSA key"); foreach (XmlNode node in xml.DocumentElement.ChildNodes) { switch (node.Name) { case "Modulus": parameters.Modulus = Convert.FromBase64String(node.InnerText); break; case "Exponent": parameters.Exponent = Convert.FromBase64String(node.InnerText); break; case "P": parameters.P = Convert.FromBase64String(node.InnerText); break; case "Q": parameters.Q = Convert.FromBase64String(node.InnerText); break; case "DP": parameters.DP = Convert.FromBase64String(node.InnerText); break; case "DQ": parameters.DQ = Convert.FromBase64String(node.InnerText); break; case "InverseQ": parameters.InverseQ = Convert.FromBase64String(node.InnerText); break; case "D": parameters.D = Convert.FromBase64String(node.InnerText); break; } } using (var rsa = RSA.Create()) { rsa.ImportParameters(parameters); var blockSize = rsa.KeySize / 8 - 42; var offset = 0; var input = Encoding.UTF32.GetBytes(plainText); StringBuilder output = new StringBuilder(); while (offset < input.Length) { var length = input.Length - offset; if (length > blockSize) length = blockSize; var block = new byte[length]; Array.Copy(input, offset, block, 0, length); var cipherText = rsa.Encrypt(block, RSAEncryptionPadding.OaepSHA1); Array.Reverse(cipherText); output.Append(Convert.ToBase64String(cipherText)); offset += length; } return output.ToString(); } } private async Task ReadBytesAsync(int count) { int totalBytesRead = 0; byte[] buffer = new byte[count]; while (totalBytesRead < count) { int bytesRead = await connectionStream.ReadAsync(buffer, totalBytesRead, count - totalBytesRead).ConfigureAwait(false); if (bytesRead == 0) throw new Exception("Connection closed unexpectedly"); totalBytesRead += bytesRead; } return buffer; } private async Task ReadMessageAsync() { byte[] buffer = await ReadBytesAsync(2).ConfigureAwait(false); int size = buffer[0] * 256 + buffer[1]; return Encoding.ASCII.GetString(await ReadBytesAsync(size).ConfigureAwait(false)); } public int ReceiveTimeout { get; set; } public int SendTimeout { get; set; } private byte[] Transform(ICryptoTransform transform, byte[] input) { using (var memoryStream = new MemoryStream()) using (var cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write)) { cryptoStream.Write(input, 0, input.Length); cryptoStream.FlushFinalBlock(); return memoryStream.ToArray(); } } private async Task WriteMessageAsync(MessageBuilder message) { byte[] data = message.GetBytes(); await connectionStream.WriteAsync(data, 0, data.Length).ConfigureAwait(false); } } }