| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- using BulkPrintingAPI.Configuration;
- using MAX.Models;
- using Microsoft.AspNetCore.Http;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.Extensions.Caching.Memory;
- using Microsoft.Extensions.Logging;
- using System;
- using System.Diagnostics;
- using System.IO;
- using System.Security.Claims;
- using System.Security.Cryptography;
- using System.Text;
- using System.Threading.Tasks;
- namespace BulkPrintingAPI.Controllers
- {
- public static class Utils
- {
- public static byte GetMinimalBitmask(byte value)
- {
- if (value == 0)
- {
- return 0;
- }
- value--;
- value |= (byte)(value >> 1);
- value |= (byte)(value >> 2);
- value |= (byte)(value >> 4);
- return value;
- }
- public static string GenerateRandomStringFromAlphabet(int length, string alphabet =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+=[{]};:<>|./?")
- {
- if (alphabet.Length > 255)
- {
- throw new ArgumentException("Alphabet too long", nameof(alphabet));
- }
- var minimalBitmask = GetMinimalBitmask((byte)alphabet.Length);
- var builder = new StringBuilder(length);
- var randomBytes = new byte[length * 2];
- int offset = randomBytes.Length;
- using (var rng = RandomNumberGenerator.Create())
- {
- while (builder.Length < length)
- {
- if (offset >= randomBytes.Length)
- {
- rng.GetBytes(randomBytes);
- offset = 0;
- }
- // We constrain c using minimalBitmask (minimal power of
- // two greater than alphabetSize, minus 1) so that it is as
- // close to fitting within alphabet.Length as possible while still
- // being evenly distributed. We then throw away any numbers >=
- // alphabet.Length. This is wasteful but it is the only way of
- // generating an unbiased selection, and random number
- // generators are very fast.
- byte c = (byte)(randomBytes[offset] & minimalBitmask);
- if (c < alphabet.Length)
- {
- builder.Append(alphabet[c]);
- }
- offset++;
- }
- }
- return builder.ToString();
- }
- public static byte[] GenerateAndAesEncryptKey(int keyLength, byte[] encryptingKey)
- {
- var key = new byte[keyLength];
- using (var rng = RandomNumberGenerator.Create())
- {
- rng.GetBytes(key);
- }
- using (var aes = Aes.Create())
- using (var encryptor = aes.CreateEncryptor(encryptingKey, new byte[16]))
- {
- return MAX.Utils.Transform(encryptor, key);
- }
- }
- public static byte[] GenerateAndAesEncryptPassword(int passwordLength, byte[] encryptingKey)
- {
- var password = GenerateRandomStringFromAlphabet(passwordLength);
- using (var aes = Aes.Create())
- using (var encryptor = aes.CreateEncryptor(encryptingKey, new byte[16]))
- {
- return MAX.Utils.Transform(encryptor, Encoding.ASCII.GetBytes(password));
- }
- }
- public static byte[] AesDecryptBytes(byte[] cipherText, byte[] key)
- {
- using (var aes = Aes.Create())
- using (var decryptor = aes.CreateDecryptor(key, new byte[16]))
- {
- return MAX.Utils.Transform(decryptor, cipherText);
- }
- }
- public static string AesDecryptString(byte[] cipherText, byte[] key)
- {
- return Encoding.ASCII.GetString(AesDecryptBytes(cipherText, key));
- }
- public static byte[] AesReEncrypt(byte[] cipherText, byte[] originalKey, byte[] newKey)
- {
- using (var aes = Aes.Create())
- {
- byte[] plainText;
- using (var decryptor = aes.CreateDecryptor(originalKey, new byte[16]))
- {
- plainText = MAX.Utils.Transform(decryptor, cipherText);
- }
- using (var encryptor = aes.CreateEncryptor(newKey, new byte[16]))
- {
- return MAX.Utils.Transform(encryptor, plainText);
- }
- }
- }
- public static async Task<MAX.LoginCredentials> SyncUserAndVendorWithDbAsync(MAXContext context,
- DataEncryptionOptions dataEncryptionOptions, User user, string password, int vendorId,
- string serialNumber)
- {
- var warehouse = user.Account.Warehouse;
- var existingWarehouse = await context.Warehouses.FindAsync(warehouse.Id).ConfigureAwait(false);
- if (existingWarehouse == null)
- {
- context.Warehouses.Add(warehouse);
- }
- else
- {
- existingWarehouse.Name = warehouse.Name;
- warehouse = existingWarehouse;
- }
- var account = user.Account;
- var existingAccount = await context.Accounts.FindAsync(account.Id).ConfigureAwait(false);
- if (existingAccount == null)
- {
- context.Accounts.Add(account);
- }
- else
- {
- existingAccount.Name = account.Name;
- existingAccount.Status = account.Status;
- existingAccount.Reference = account.Reference;
- existingAccount.Balance = account.Balance;
- account = existingAccount;
- }
- account.Warehouse = warehouse;
- var existingUser = await context.Users.FindAsync(user.Id).ConfigureAwait(false);
- if (existingUser == null)
- {
- context.Users.Add(user);
- }
- else
- {
- existingUser.Username = user.Username;
- existingUser.FirstName = user.FirstName;
- existingUser.Surname = user.Surname;
- existingUser.Enabled = user.Enabled;
- existingUser.Level = user.Level;
- existingUser.System = user.System;
- existingUser.LastLogin = user.LastLogin;
- existingUser.CanPrintOffline = user.CanPrintOffline;
- existingUser.CanPrintOnline = user.CanPrintOnline;
- existingUser.CanReprintOffline = user.CanReprintOffline;
- existingUser.CanReprintOnline = user.CanReprintOnline;
- existingUser.OfflinePrintValue = user.OfflinePrintValue;
- existingUser.OfflineReprintValue = user.OfflineReprintValue;
- existingUser.OnlinePrintValue = user.OnlinePrintValue;
- existingUser.OnlineReprintValue = user.OnlineReprintValue;
- existingUser.BulkExport = user.BulkExport;
- existingUser.BulkExportMaxValue = user.BulkExportMaxValue;
- existingUser.BulkOrder = user.BulkOrder;
- existingUser.BulkOrderMaxValue = user.BulkOrderMaxValue;
- existingUser.BulkViewPins = user.BulkViewPins;
- existingUser.BulkReExport = user.BulkReExport;
- user = existingUser;
- }
- user.Account = account;
- var vendor = await context.Vendors.FindAsync(vendorId).ConfigureAwait(false);
- if (vendor == null)
- {
- vendor = new Vendor()
- {
- Id = vendorId,
- SerialNumber = serialNumber,
- Account = account,
- EncryptedDatabasePassword =
- // A password length of 31 bytes results keeps us inside 2 AES blocks (32 bytes)
- // as padding is a minimum of 1 byte.
- GenerateAndAesEncryptPassword(31, dataEncryptionOptions.DefaultKey),
- EncryptedVoucherKey =
- GenerateAndAesEncryptKey(24, dataEncryptionOptions.DefaultKey)
- };
- context.Add(vendor);
- }
- else
- {
- vendor.SerialNumber = serialNumber;
- vendor.Account = account;
- }
- await context.SaveChangesAsync().ConfigureAwait(false);
- return new MAX.LoginCredentials
- {
- User = user,
- Vendor = vendor,
- Password = password
- };
- }
- public static async Task<MAX.LoginCredentials> GetLoginCredentialsFromRequestAsync(
- HttpContext httpContext, MAXContext dbContext)
- {
- var authInfo = await httpContext.Authentication.GetAuthenticateInfoAsync("Bearer");
- if (authInfo == null)
- {
- throw new Exception("Not authenticated");
- }
- int userId = -1;
- int vendorId = -1;
- string password = null;
- foreach (var claim in authInfo.Principal.Claims)
- {
- switch (claim.Type)
- {
- case ClaimTypes.NameIdentifier:
- userId = int.Parse(claim.Value);
- break;
- case "xvid":
- vendorId = int.Parse(claim.Value);
- break;
- case "xpwd":
- password = claim.Value;
- break;
- }
- }
- var credentials = new MAX.LoginCredentials()
- {
- User = await dbContext.Users
- .Include(u => u.Account)
- .ThenInclude(a => a.Warehouse)
- .SingleOrDefaultAsync(u => u.Id == userId)
- .ConfigureAwait(false),
- Vendor = await dbContext.Vendors
- .Include(v => v.Account) // Technically not necessary as the account is loaded above
- .SingleOrDefaultAsync(v => v.Id == vendorId)
- .ConfigureAwait(false),
- Password = password
- };
- if ((credentials.User == null) || (credentials.Vendor == null))
- {
- throw new Exception(String.Format(
- "Missing user or vendor information for userId={0} vendorId={1}",
- userId, vendorId));
- }
- return credentials;
- }
- public static async Task<ProductCatalogue> GetProductCatalogueAsync(
- MAX.ClientFactory clientFactory, ILogger logger, IMemoryCache cache,
- MAX.LoginCredentials credentials, bool forceRefresh)
- {
- var key = CacheKeys.GetAccountProductsKey(credentials.User.AccountId);
- if (forceRefresh)
- {
- cache.Remove(key);
- }
- return await cache.GetOrCreateAsync(key, entry =>
- {
- // TODO: set expiry time, etc.
- return MAX.Utils.GetProductCatalogueAsync(clientFactory, logger, credentials);
- }).ConfigureAwait(false);
- }
- [Conditional("DEBUG")]
- private static void VoucherSanityCheck(Batch batch, decimal faceValue, int batchId,
- string productDescription)
- {
- if (batch.FaceValue != faceValue)
- {
- throw new Exception("Voucher face value mismatch");
- }
- if (batch.Id != batchId)
- {
- throw new Exception("Voucher batch ID mismatch");
- }
- if (batch.ProductDescription != productDescription)
- {
- throw new Exception("Voucher product description mismatch");
- }
- }
- public static async Task DownloadVouchersAsync(SFTPOptions sftpOptions, MAXContext context,
- ILogger logger, Batch batch)
- {
- var remoteFileName = string.Format("{0}_{1}.dat", batch.Account.Id, batch.Id);
- using (var voucherStream = new MemoryStream())
- {
- var connectionInfo = new Renci.SshNet.ConnectionInfo(
- sftpOptions.Host,
- sftpOptions.Port,
- sftpOptions.Username,
- new Renci.SshNet.PasswordAuthenticationMethod(sftpOptions.Username, sftpOptions.Password)
- );
- connectionInfo.Timeout = TimeSpan.FromSeconds(sftpOptions.ConnectTimeout);
- await Task.Run(() =>
- {
- using (var sshClient = new Renci.SshNet.SftpClient(connectionInfo))
- {
- sshClient.Connect();
- sshClient.DownloadFile(remoteFileName, voucherStream);
- }
- });
- voucherStream.Position = 0;
- using (var streamReader = new StreamReader(voucherStream))
- {
- while (streamReader.Peek() >= 0)
- {
- var line = streamReader.ReadLine();
- var parts = line.Split('|');
- VoucherSanityCheck(batch, decimal.Parse(parts[4]), int.Parse(parts[5]),
- parts[7]);
- context.Add(new Voucher()
- {
- Id = int.Parse(parts[0]),
- ExpiryDate = DateTime.Parse(parts[1]),
- Serial = parts[2],
- EncryptedPIN = parts[3],
- SequenceNumber = int.Parse(parts[6]),
- Batch = batch
- });
- }
- }
- batch.ReadyForDownload = true;
- await context.SaveChangesAsync().ConfigureAwait(false);
- try
- {
- await Task.Run(() =>
- {
- using (var sshClient = new Renci.SshNet.SftpClient(connectionInfo))
- {
- sshClient.Connect();
- sshClient.DeleteFile(remoteFileName);
- }
- });
- }
- catch (Exception e)
- {
- logger.LogWarning("Failed to delete file on FTP server: {0}: {1}", remoteFileName, e.Message);
- }
- }
- }
- }
- }
|