This commit is contained in:
523
server/biz/KeyFactory.cs
Normal file
523
server/biz/KeyFactory.cs
Normal file
@@ -0,0 +1,523 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Org.BouncyCastle.Security;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.OpenSsl;
|
||||
using Sockeye.Models;
|
||||
using Sockeye.Util;
|
||||
|
||||
|
||||
|
||||
namespace Sockeye.Biz
|
||||
{
|
||||
//Key generator controller
|
||||
public static class KeyFactory
|
||||
{
|
||||
|
||||
public const string PLUGIN_MBI_KEY = "MBI - Minimal browser interface";
|
||||
public const string PLUGIN_WBI_KEY = "WBI - Web browser interface";
|
||||
public const string PLUGIN_QBI_KEY = "QBI - QuickBooks interface";
|
||||
public const string PLUGIN_QBOI_KEY = "QBOI - QuickBooks Online interface";
|
||||
public const string PLUGIN_PTI_KEY = "PTI - US Sage 50/Peachtree interface";
|
||||
public const string PLUGIN_QUICK_NOTIFICATION_KEY = "QuickNotification";
|
||||
public const string PLUGIN_EXPORT_TO_XLS_KEY = "ExportToXls";
|
||||
public const string PLUGIN_OUTLOOK_SCHEDULE_KEY = "OutlookSchedule";
|
||||
public const string PLUGIN_OLI_KEY = "AyaNovaOLI";
|
||||
public const string PLUGIN_IMPORT_EXPORT_CSV_DUPLICATE_KEY = "ImportExportCSVDuplicate";
|
||||
public const string PLUGIN_RI_KEY = "RI - Responsive Interface";
|
||||
|
||||
|
||||
private static Dictionary<string, DateTime> _plugins;
|
||||
|
||||
//Generate a key message reply from a key selection object
|
||||
//CALLED BY LicenseController Generate route
|
||||
public static string GetKeyReply(dtoKeyOptions ko, LicenseTemplates t, rockfishContext ct)
|
||||
{
|
||||
|
||||
//case 3542
|
||||
ko.installByDate = System.DateTime.Now.AddYears(1);//changed from one month to one year
|
||||
string sKey = genKey(ko, ct);
|
||||
string sMsg = genMessage(sKey, ko, t);
|
||||
return sMsg;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//called by trialkeyrequesthandler.GenerateFromRequest
|
||||
//get a trial key for named regTo
|
||||
public static string GetTrialKey(string regTo, bool lite, LicenseTemplates t, string authorizedUserKeyGeneratorStamp,
|
||||
string emailAddress, rockfishContext ct)//case 3233
|
||||
{
|
||||
DateTime dtoneMonth = System.DateTime.Now.AddMonths(1);
|
||||
long oneMonth = DateUtil.DateToEpoch(System.DateTime.Now.AddMonths(1));
|
||||
|
||||
dtoKeyOptions ko = new dtoKeyOptions();
|
||||
|
||||
//case 3233
|
||||
ko.emailAddress = emailAddress;
|
||||
ko.customerId = 0; //not a customer so trial 0
|
||||
|
||||
ko.registeredTo = regTo;
|
||||
ko.supportExpiresDate = oneMonth;
|
||||
ko.isLite = lite;
|
||||
ko.installByDate = dtoneMonth;
|
||||
ko.authorizedUserKeyGeneratorStamp = authorizedUserKeyGeneratorStamp;
|
||||
if (lite)
|
||||
{
|
||||
ko.users = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
ko.users = 5;
|
||||
}
|
||||
|
||||
ko.licenseType = "webRequestedTrial";
|
||||
|
||||
ko.qbi = true;
|
||||
ko.qbiSupportExpiresDate = oneMonth;
|
||||
|
||||
ko.qboi = true;
|
||||
ko.qboiSupportExpiresDate = oneMonth;
|
||||
|
||||
ko.pti = true;
|
||||
ko.ptiSupportExpiresDate = oneMonth;
|
||||
|
||||
ko.exportToXls = true;
|
||||
ko.exportToXlsSupportExpiresDate = oneMonth;
|
||||
|
||||
ko.outlookSchedule = true;
|
||||
ko.outlookScheduleSupportExpiresDate = oneMonth;
|
||||
|
||||
ko.oli = true;
|
||||
ko.oliSupportExpiresDate = oneMonth;
|
||||
|
||||
ko.importExportCSVDuplicate = true;
|
||||
ko.importExportCSVDuplicateSupportExpiresDate = oneMonth;
|
||||
|
||||
if (!lite)
|
||||
{
|
||||
ko.quickNotification = true;
|
||||
ko.quickNotificationSupportExpiresDate = oneMonth;
|
||||
|
||||
ko.mbi = true;
|
||||
ko.mbiSupportExpiresDate = oneMonth;
|
||||
|
||||
ko.wbi = true;
|
||||
ko.wbiSupportExpiresDate = oneMonth;
|
||||
|
||||
ko.ri = true;
|
||||
ko.riSupportExpiresDate = oneMonth;
|
||||
}
|
||||
|
||||
string sKey = genKey(ko, ct);
|
||||
string sMsg = genMessage(sKey, ko, t);
|
||||
return sMsg;
|
||||
|
||||
}
|
||||
|
||||
|
||||
//Take the key and the options and make a return message ready to send
|
||||
private static string genMessage(string sKey, dtoKeyOptions ko, LicenseTemplates template)
|
||||
{
|
||||
string sMessage = "";
|
||||
|
||||
if (ko.licenseType == "new")
|
||||
{
|
||||
if (ko.isLite)
|
||||
{
|
||||
sMessage = template.LiteNew;
|
||||
}
|
||||
else
|
||||
{
|
||||
sMessage = template.FullNew;
|
||||
}
|
||||
}
|
||||
else if (ko.licenseType == "addon")
|
||||
{
|
||||
if (ko.isLite)
|
||||
{
|
||||
sMessage = template.LiteAddOn;
|
||||
}
|
||||
else
|
||||
{
|
||||
sMessage = template.FullAddOn;
|
||||
}
|
||||
}
|
||||
else//licensed trial
|
||||
{
|
||||
if (ko.isLite)
|
||||
{
|
||||
sMessage = template.LiteTrial;
|
||||
}
|
||||
else
|
||||
{
|
||||
sMessage = template.FullTrial;
|
||||
}
|
||||
}
|
||||
|
||||
//token substitutions
|
||||
sMessage = sMessage.Replace("[LicenseExpiryDate]", ko.installByDate.ToString("D"));//https://github.com/dotnet/coreclr/issues/2317
|
||||
sMessage = sMessage.Replace("[LicenseDescription]", LicenseInfo(ko));
|
||||
sMessage = sMessage.Replace("[LicenseKey]", sKey);
|
||||
|
||||
return sMessage;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Extra info to display about key at top of key message
|
||||
private static string LicenseInfo(dtoKeyOptions ko)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.Append("LICENSE DETAILS\r\n");
|
||||
// sb.Append("This key must be installed before: ");
|
||||
// sb.Append(ko.installByDate.ToString("D"));
|
||||
// sb.Append("\r\n");
|
||||
|
||||
//if (kg.SelectedLicenseType == "Web requested trial")
|
||||
//{
|
||||
// sb.Append("*** This temporary license key has been provided for limited evaluation purposes only *** \r\n");
|
||||
// sb.Append("This license will expire and AyaNova usage will be restricted after: " + kg.Expires.ToLongDateString() + "\r\n\r\n");
|
||||
//}
|
||||
|
||||
if (ko.keyWillLockout)
|
||||
{
|
||||
sb.Append("*** This temporary license key is provided for evaluation use only pending payment ***\r\n");
|
||||
sb.Append("This license will expire and AyaNova usage will be restricted after: " + DateUtil.EpochToString(ko.lockoutDate) + "\r\n");
|
||||
sb.Append("\r\n");
|
||||
sb.Append("A permanent license key will be sent to you when payment \r\n" +
|
||||
"has been received and processed. There will be no extensions or \r\n" +
|
||||
"exceptions. Please send in payment early enough to allow for \r\n" +
|
||||
"mail and processing time to ensure uninterrupted use of AyaNova" + (ko.isLite ? " Lite" : "") + ". \r\n\r\n");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
sb.Append("Registered to: ");
|
||||
sb.Append(ko.registeredTo);
|
||||
sb.Append("\r\n");
|
||||
|
||||
//case 3233
|
||||
sb.Append("Fetch address: ");
|
||||
sb.Append(ko.emailAddress);
|
||||
sb.Append("\r\n");
|
||||
|
||||
sb.Append("Fetch code: ");
|
||||
sb.Append(ko.fetchCode);
|
||||
sb.Append("\r\n");
|
||||
|
||||
|
||||
sb.Append("Scheduleable resources: ");
|
||||
switch (ko.users)
|
||||
{
|
||||
case 1:
|
||||
sb.AppendLine("1");
|
||||
break;
|
||||
case 5:
|
||||
sb.AppendLine("Up to 5");
|
||||
break;
|
||||
case 10:
|
||||
sb.AppendLine("Up to 10");
|
||||
break;
|
||||
case 15:
|
||||
sb.AppendLine("Up to 15");//case 3550
|
||||
break;
|
||||
case 20:
|
||||
sb.AppendLine("Up to 20");
|
||||
break;
|
||||
case 50:
|
||||
sb.AppendLine("Up to 50");
|
||||
break;
|
||||
case 999:
|
||||
sb.AppendLine("Up to 999");
|
||||
break;
|
||||
}
|
||||
|
||||
sb.AppendLine("Support and updates until: " + DateUtil.EpochToString(ko.supportExpiresDate) + "\r\n");
|
||||
|
||||
if (_plugins.Count > 0)
|
||||
{
|
||||
sb.Append("\r\n");
|
||||
sb.Append("Plugins:\r\n");
|
||||
foreach (KeyValuePair<string, DateTime> kv in _plugins)
|
||||
{
|
||||
sb.Append("\t");
|
||||
sb.Append(kv.Key);
|
||||
sb.Append(" support and updates until: ");
|
||||
sb.Append(kv.Value.ToString("D"));
|
||||
sb.Append("\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static string GenFetchCode()
|
||||
{
|
||||
|
||||
//sufficient for this purpose
|
||||
//https://stackoverflow.com/a/1344258/8939
|
||||
|
||||
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
var stringChars = new char[10];
|
||||
var random = new Random();
|
||||
|
||||
for (int i = 0; i < stringChars.Length; i++)
|
||||
{
|
||||
stringChars[i] = chars[random.Next(chars.Length)];
|
||||
}
|
||||
|
||||
var finalString = new String(stringChars);
|
||||
return finalString;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Generate keycode based on passed in data
|
||||
/// This is called by both regular and trial license key routes
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static string genKey(License l)
|
||||
{
|
||||
_plugins = new Dictionary<string, DateTime>();
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
StringBuilder sbKey = new StringBuilder();
|
||||
StringWriter sw = new StringWriter(sbKey);
|
||||
|
||||
|
||||
l.FetchCode = GenFetchCode();
|
||||
|
||||
using (Newtonsoft.Json.JsonWriter w = new Newtonsoft.Json.JsonTextWriter(sw))
|
||||
{
|
||||
w.Formatting = Newtonsoft.Json.Formatting.Indented;
|
||||
|
||||
//outer object start
|
||||
w.WriteStartObject();
|
||||
w.WritePropertyName("AyaNovaLicenseKey");
|
||||
|
||||
w.WriteStartObject();//start of key object
|
||||
|
||||
w.WritePropertyName("SchemaVersion");
|
||||
w.WriteValue("7");
|
||||
|
||||
//stamp a unique value in the key so it can be revoked later
|
||||
//used to use the digest value of the key for this with xml key
|
||||
//whole unix timestamp seconds but kept as a double to work beyond 2038
|
||||
w.WritePropertyName("Id");
|
||||
var vv = Math.Truncate((DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds);
|
||||
string sId = vv.ToString();
|
||||
if (sId.Contains(","))
|
||||
sId = sId.Split('.')[0];
|
||||
w.WriteValue(sId);
|
||||
|
||||
w.WritePropertyName("Created");
|
||||
w.WriteValue(System.DateTime.Now);
|
||||
|
||||
w.WritePropertyName("Sub");
|
||||
w.WriteValue("true");
|
||||
|
||||
w.WritePropertyName("RegisteredTo");
|
||||
w.WriteValue(l.RegTo);
|
||||
|
||||
//case 3233
|
||||
w.WritePropertyName("EmailAddress");
|
||||
w.WriteValue(l.FetchEmail);
|
||||
|
||||
w.WritePropertyName("FetchCode");
|
||||
|
||||
w.WriteValue(l.FetchCode);
|
||||
|
||||
//case 3187 - Source here
|
||||
//rockfish
|
||||
w.WritePropertyName("Source");
|
||||
w.WriteValue(StringUtil.ToHex("RFID" + "420"));
|
||||
|
||||
|
||||
w.WritePropertyName("InstallableUntil");
|
||||
w.WriteValue(l.MaintenanceExpire);
|
||||
|
||||
w.WritePropertyName("TotalScheduleableUsers");
|
||||
w.WriteValue(l.Users.ToString());//Needs to be a string to match rockfish format
|
||||
|
||||
w.WritePropertyName("Expires");
|
||||
w.WriteValue(l.MaintenanceExpire);
|
||||
|
||||
if (l.LicenseExpire!=null)
|
||||
{
|
||||
w.WritePropertyName("LockDate");
|
||||
w.WriteValue(l.LicenseExpire);
|
||||
|
||||
}
|
||||
|
||||
w.WritePropertyName("RequestedTrial");
|
||||
w.WriteValue(l.TrialMode.ToString());
|
||||
|
||||
//PLUGINS
|
||||
w.WritePropertyName("Plugins");
|
||||
w.WriteStartObject();//start of key object
|
||||
w.WritePropertyName("Plugin");
|
||||
w.WriteStartArray();
|
||||
|
||||
if (l.MBI)
|
||||
AddLicensePlugin(w, PLUGIN_MBI_KEY, l.MBIExpires);
|
||||
|
||||
if (l.WBI)
|
||||
AddLicensePlugin(w, PLUGIN_WBI_KEY, l.WBIExpires);
|
||||
|
||||
if (l.QBI)
|
||||
AddLicensePlugin(w, PLUGIN_QBI_KEY, l.QBIExpires);
|
||||
|
||||
if (l.QBOI)
|
||||
AddLicensePlugin(w, PLUGIN_QBOI_KEY, l.QBOIExpires);
|
||||
|
||||
if (l.PTI)
|
||||
AddLicensePlugin(w, PLUGIN_PTI_KEY, l.PTIExpires);
|
||||
|
||||
if (l.QuickNotification)
|
||||
AddLicensePlugin(w, PLUGIN_QUICK_NOTIFICATION_KEY, l.QuickNotificationExpires);
|
||||
|
||||
if (l.ExportToXLS)
|
||||
AddLicensePlugin(w, PLUGIN_EXPORT_TO_XLS_KEY, l.ExportToXLSExpires);
|
||||
|
||||
if (l.OutlookSchedule)
|
||||
AddLicensePlugin(w, PLUGIN_OUTLOOK_SCHEDULE_KEY, l.OutlookScheduleExpires);
|
||||
|
||||
if (l.OLI)
|
||||
AddLicensePlugin(w, PLUGIN_OLI_KEY, l.OLIExpires);
|
||||
|
||||
if (l.ImportExportCSVDuplicate)
|
||||
AddLicensePlugin(w, PLUGIN_IMPORT_EXPORT_CSV_DUPLICATE_KEY, l.ImportExportCSVDuplicateExpires);
|
||||
|
||||
if (l.RI)
|
||||
AddLicensePlugin(w, PLUGIN_RI_KEY, l.RIExpires);
|
||||
|
||||
//end of plugins array
|
||||
w.WriteEnd();
|
||||
|
||||
//end of plugins object
|
||||
w.WriteEndObject();
|
||||
|
||||
//end of AyaNova/AyaNovaLite key object
|
||||
w.WriteEndObject();
|
||||
|
||||
//close outer 'wrapper' object brace }
|
||||
w.WriteEndObject();
|
||||
|
||||
}//end of using statement
|
||||
|
||||
|
||||
// ## CALCULATE SIGNATURE
|
||||
|
||||
//GET JSON as a string with whitespace stripped outside of delimited strings
|
||||
//http://stackoverflow.com/questions/8913138/minify-indented-json-string-in-net
|
||||
string keyNoWS = System.Text.RegularExpressions.Regex.Replace(sbKey.ToString(), "(\"(?:[^\"\\\\]|\\\\.)*\")|\\s+", "$1");
|
||||
|
||||
|
||||
//**** Note this is our real 2016 private key
|
||||
var privatePEM = @"-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAz7wrvLDcKVMZ31HFGBnLWL08IodYIV5VJkKy1Z0n2snprhSi
|
||||
u3izxTyz+SLpftvKHJpky027ii7l/pL9Bo3JcjU5rKrxXavnE7TuYPjXn16dNLd0
|
||||
K/ERSU+pXLmUaVN0nUWuGuUMoGJMEXoulS6pJiG11yu3BM9fL2Nbj0C6a+UwzEHF
|
||||
mns3J/daZOb4gAzMUdJfh9OJ0+wRGzR8ZxyC99Na2gDmqYglUkSMjwLTL/HbgwF4
|
||||
OwmoQYJBcET0Wa6Gfb17SaF8XRBV5ZtpCsbStkthGeoXZkFriB9c1eFQLKpBYQo2
|
||||
DW3H1MPG2nAlQZLbkJj5cSh7/t1bRF08m6P+EQIDAQABAoIBAQCGvTpxLRXgB/Kk
|
||||
EtmQBEsMx9EVZEwZeKIqKuDsBP8wvf4/10ql5mhT6kehtK9WhSDW5J2z8DtQKZMs
|
||||
SBKuCZE77qH2CPp9E17SPWzQoRbaW/gDlWpYhgf8URs89XH5zxO4XtXKw/4omRlV
|
||||
zLYiNR2pifv0EHqpOAg5KGzewdEo4VgXgtRWpHZLMpH2Q0/5ZIKMhstI6vFHP1p7
|
||||
jmU4YI6uxiu7rVrZDmIUsAGoTdMabNqK/N8hKaoBiIto0Jn1ck26g+emLg8m160y
|
||||
Xciu5yFUU+PP1SJMUs+k1UnAWf4p46X9jRLQCBRue9o0Ntiq/75aljRoDvgdwDsR
|
||||
mg4ZANqxAoGBAPBoM5KoMZ4sv8ZFv8V+V8hgL5xiLgGoiwQl91mRsHRM/NQU5A/w
|
||||
tH8nmwUrJOrksV7kX9228smKmoliTptyGGyi1NPmSkA7cN9YYnENoOEBHCVNK9vh
|
||||
P+bkbMYUDNMW4fgOj09oXtQtMl5E2B3OTGoNwZ2w13YQJ8RIniLPsX7nAoGBAN01
|
||||
eQNcUzQk9YrFGTznOs8udDLBfigDxaNnawvPueulJdBy6ZXDDrKmkQQA7xxl8YPr
|
||||
dNtBq2lOgnb6+smC15TaAfV/fb8BLmkSwdn4Fy0FApIXIEOnLq+wjkte98nuezl8
|
||||
9KXDzaqNI9hPuk2i36tJuLLMH8hzldveWbWjSlRHAoGBAKRPE7CQtBjfjNL+qOta
|
||||
RrT0yJWhpMANabYUHNJi+K8ET2jEPnuGkFa3wwPtUPYaCABLJhprB9Unnid3wTIM
|
||||
8RSO1ddd9jGgbqy3w9Bw+BvQnmQAMpG9iedNB+r5mSpM4XSgvuIO+4EYwuwbMXpt
|
||||
nVx+um4Eh75xnDxTRYGVYkLRAoGAaZVpUlpR+HSfooHbPv+bSWKB4ewLPCw4vHrT
|
||||
VErtEfW8q9b9eRcmP81TMFcFykc6VN4g47pfh58KlKHM7DwAjDLWdohIy89TiKGE
|
||||
V3acEUfv5y0UoFX+6ara8Ey+9upWdKUY3Lotw3ckoc3EPeQ84DQK7YSSswnAgLaL
|
||||
mS/8fWcCgYBjRefVbEep161d2DGruk4X7eNI9TFJ278h6ydW5kK9aTJuxkrtKIp4
|
||||
CYf6emoB4mLXFPvAmnsalkhN2iB29hUZCXXSUjpKZrpijL54Wdu2S6ynm7aT97NF
|
||||
oArP0E2Vbow3JMxq/oeXmHbrLMLQfYyXwFmciLFigOtkd45bfHdrbA==
|
||||
-----END RSA PRIVATE KEY-----";
|
||||
|
||||
PemReader pr = new PemReader(new StringReader(privatePEM));
|
||||
AsymmetricCipherKeyPair keys = (AsymmetricCipherKeyPair)pr.ReadObject();
|
||||
var encoder = new UTF8Encoding(false, true);
|
||||
var inputData = encoder.GetBytes(keyNoWS);
|
||||
var signer = SignerUtilities.GetSigner("SHA256WITHRSA");
|
||||
signer.Init(true, keys.Private);
|
||||
signer.BlockUpdate(inputData, 0, inputData.Length);
|
||||
var sign = signer.GenerateSignature();
|
||||
var signature = Convert.ToBase64String(sign);
|
||||
|
||||
|
||||
System.Text.StringBuilder sbOut = new StringBuilder();
|
||||
sbOut.AppendLine("[KEY");
|
||||
sbOut.AppendLine(sbKey.ToString());
|
||||
sbOut.AppendLine("KEY]");
|
||||
sbOut.AppendLine("[SIGNATURE");
|
||||
sbOut.AppendLine(signature);
|
||||
sbOut.AppendLine("SIGNATURE]");
|
||||
|
||||
// //case 3233 insert into db
|
||||
// License l = new License();
|
||||
// l.DtCreated = DateUtil.NowAsEpoch();
|
||||
// l.Code = ko.fetchCode;
|
||||
// l.CustomerId = ko.customerId;
|
||||
// l.Email = ko.emailAddress.ToLowerInvariant();
|
||||
// l.Key = sbOut.ToString();
|
||||
// l.RegTo = ko.registeredTo;
|
||||
// ct.License.Add(l);
|
||||
// ct.SaveChanges();
|
||||
|
||||
return sbOut.ToString();
|
||||
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static void AddLicensePlugin(Newtonsoft.Json.JsonWriter w, string pluginName, DateTime? pluginExpires)
|
||||
{
|
||||
|
||||
//this dictionary is used by the additional message code to
|
||||
//make the human readable portion of the license
|
||||
_plugins.Add(pluginName, (DateTime)pluginExpires);
|
||||
|
||||
//this is adding it to the actual key
|
||||
w.WriteStartObject();
|
||||
w.WritePropertyName("Item");
|
||||
w.WriteValue(pluginName);
|
||||
|
||||
w.WritePropertyName("SubscriptionExpires");
|
||||
w.WriteValue(pluginExpires);
|
||||
|
||||
w.WriteEndObject();
|
||||
//----------------
|
||||
}
|
||||
|
||||
|
||||
|
||||
//eoc
|
||||
}
|
||||
//eons
|
||||
}
|
||||
@@ -48,7 +48,9 @@ namespace Sockeye.Biz
|
||||
return null;
|
||||
else
|
||||
{
|
||||
GENERATE KEY AND SET FETCH CODE IF APPLICABLE HERE
|
||||
await GenerateKey(newObject);
|
||||
if (HasErrors) return null;
|
||||
|
||||
|
||||
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
||||
await ct.License.AddAsync(newObject);
|
||||
@@ -453,7 +455,9 @@ namespace Sockeye.Biz
|
||||
putObject.Tags = TagBiz.NormalizeTags(putObject.Tags);
|
||||
await ValidateAsync(putObject, dbObject);
|
||||
if (HasErrors) return null;
|
||||
GENERATE KEY AND SET FETCH CODE IF APPLICABLE HERE
|
||||
await GenerateKey(putObject);
|
||||
if (HasErrors) return null;
|
||||
|
||||
ct.Replace(dbObject, putObject);
|
||||
try
|
||||
{
|
||||
@@ -474,6 +478,40 @@ namespace Sockeye.Biz
|
||||
return putObject;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region KEY GEN
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//GENERATE
|
||||
//
|
||||
internal async Task GenerateKey(License l)
|
||||
{
|
||||
|
||||
if(l.CustomerId!=null){
|
||||
l.CustomerViz=await ct.Customer.AsNoTracking().Where(x => x.Id == l.CustomerId).Select(x => x.Name).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
switch(l.PGroup){
|
||||
case ProductGroup.RavenPerpetual:
|
||||
{
|
||||
|
||||
}
|
||||
break;
|
||||
case ProductGroup.RavenSubscription:
|
||||
{
|
||||
|
||||
}
|
||||
break;
|
||||
case ProductGroup.AyaNova7:
|
||||
{
|
||||
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
#endregion key gen
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//DELETE
|
||||
//
|
||||
|
||||
460
server/biz/RavenKeyFactory.cs
Normal file
460
server/biz/RavenKeyFactory.cs
Normal file
@@ -0,0 +1,460 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Org.BouncyCastle.Security;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.OpenSsl;
|
||||
using Sockeye.Util;
|
||||
|
||||
|
||||
|
||||
|
||||
namespace Sockeye.Biz
|
||||
{
|
||||
//Key generator controller
|
||||
public static class RavenKeyFactory
|
||||
{
|
||||
//Sept 2022 decided all trial periods are 7 days, they can keep renewing a new trial by erasing data or request a longer period case by case
|
||||
public const int TRIAL_PERIOD_DAYS = 7;
|
||||
|
||||
|
||||
|
||||
//Unlicensed token
|
||||
private const string UNLICENSED_TOKEN = "UNLICENSED";
|
||||
|
||||
//REVOKED token
|
||||
public const string REVOKED_TOKEN = "REVOKED";
|
||||
|
||||
//LICENSE USER COUNT FEATURES
|
||||
|
||||
|
||||
|
||||
//SUBSCRIPTION
|
||||
private const string ACTIVE_INTERNAL_USERS_FEATURE_NAME = "ActiveInternalUsers";
|
||||
private const string ACTIVE_CUSTOMER_USERS_FEATURE_NAME = "ActiveCustomerUsers";
|
||||
private const string MAXIMUM_DATA_GB_FEATURE_NAME = "MaximumDataGB";
|
||||
|
||||
|
||||
//Add-on's / integrations
|
||||
|
||||
//This feature name means it's a trial key
|
||||
private const string TRIAL_FEATURE_NAME = "TrialMode";
|
||||
|
||||
|
||||
#region license classes
|
||||
|
||||
//DTO object returned on license query
|
||||
public class LicenseFeature
|
||||
{
|
||||
//name of feature / product
|
||||
public string Feature { get; set; }
|
||||
|
||||
//Optional count for items that require it
|
||||
public long Count { get; set; }
|
||||
|
||||
}
|
||||
|
||||
//DTO object for parsed key
|
||||
internal class AyaNovaLicenseKey
|
||||
{
|
||||
public AyaNovaLicenseKey()
|
||||
{
|
||||
Features = new List<LicenseFeature>();
|
||||
RegisteredTo = UNLICENSED_TOKEN;
|
||||
//Id = RegisteredTo;
|
||||
LicenseFormat = "8";
|
||||
|
||||
var vv = Math.Truncate((DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds);
|
||||
string sId = vv.ToString();
|
||||
if (sId.Contains(","))
|
||||
sId = sId.Split('.')[0];
|
||||
Id = sId;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
||||
System.Text.StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine($"Registered to: {RegisteredTo}");
|
||||
sb.AppendLine($"Database id: {DbId}");
|
||||
sb.AppendLine($"Type: {(Perpetual ? "Perpetual" : "Subscription")}");
|
||||
if (WillExpire)
|
||||
sb.AppendLine($"Available for use until: {LicenseExpiration.ToLongDateString()}");
|
||||
if (Perpetual)
|
||||
sb.AppendLine($"Support and updates available until: {MaintenanceExpiration.ToLongDateString()}");
|
||||
foreach (LicenseFeature f in Features)
|
||||
{
|
||||
if (f.Feature == TRIAL_FEATURE_NAME)
|
||||
{
|
||||
sb.AppendLine("Temporary license for evaluation");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
//default for items added later not tokenized
|
||||
if (f.Count > 0)
|
||||
sb.AppendLine($"{f.Feature}: {f.Count}");
|
||||
else
|
||||
sb.AppendLine($"{f.Feature}");
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
public bool IsEmpty
|
||||
{
|
||||
get
|
||||
{
|
||||
//Key is empty if it's not registered to anyone or there are no features in it
|
||||
return string.IsNullOrWhiteSpace(RegisteredTo) || (Features == null || Features.Count == 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetch the license status of the feature in question
|
||||
/// </summary>
|
||||
/// <param name="Feature"></param>
|
||||
/// <returns>LicenseFeature object or null if there is no license</returns>
|
||||
public LicenseFeature GetLicenseFeature(string Feature)
|
||||
{
|
||||
if (IsEmpty)
|
||||
return null;
|
||||
|
||||
string lFeature = Feature.ToLowerInvariant();
|
||||
|
||||
foreach (LicenseFeature l in Features)
|
||||
{
|
||||
if (l.Feature.ToLowerInvariant() == lFeature)
|
||||
{
|
||||
return l;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check for the existance of license feature
|
||||
/// </summary>
|
||||
/// <param name="Feature"></param>
|
||||
/// <returns>bool</returns>
|
||||
public bool HasLicenseFeature(string Feature)
|
||||
{
|
||||
if (IsEmpty)
|
||||
return false;
|
||||
|
||||
string lFeature = Feature.ToLowerInvariant();
|
||||
|
||||
foreach (LicenseFeature l in Features)
|
||||
{
|
||||
if (l.Feature.ToLowerInvariant() == lFeature)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public bool WillExpire
|
||||
{
|
||||
get
|
||||
{
|
||||
return LicenseExpiration < DateUtil.EmptyDateValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool LicenseExpired
|
||||
{
|
||||
get
|
||||
{
|
||||
return LicenseExpiration > DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
public bool MaintenanceExpired
|
||||
{
|
||||
get
|
||||
{
|
||||
return MaintenanceExpiration > DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool TrialLicense
|
||||
{
|
||||
get
|
||||
{
|
||||
return HasLicenseFeature(TRIAL_FEATURE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public string LicenseFormat { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string RegisteredTo { get; set; }
|
||||
public string DbId { get; set; }
|
||||
public bool Perpetual { get; set; }
|
||||
public DateTime LicenseExpiration { get; set; }
|
||||
public DateTime MaintenanceExpiration { get; set; }
|
||||
public List<LicenseFeature> Features { get; set; }
|
||||
|
||||
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static string GetRavenTrialKey(string dbid, string CompanyName, bool Perpetual)
|
||||
{
|
||||
|
||||
//Build a sample test key, sign it and return it
|
||||
AyaNovaLicenseKey k = new AyaNovaLicenseKey();
|
||||
k.LicenseFormat = "8";
|
||||
k.RegisteredTo = CompanyName;
|
||||
k.DbId = dbid;
|
||||
k.Perpetual = Perpetual;
|
||||
|
||||
//flag as trial key not regular key
|
||||
k.Features.Add(new LicenseFeature() { Feature = TRIAL_FEATURE_NAME, Count = 0 });
|
||||
if (Perpetual)
|
||||
{
|
||||
//trial period time limit
|
||||
|
||||
|
||||
//Normal code, uncomment this when done testing expiration dates of perpetual
|
||||
k.MaintenanceExpiration = k.LicenseExpiration = DateTime.UtcNow.AddDays(TRIAL_PERIOD_DAYS);
|
||||
|
||||
//Testing code for 3 minute long evaluation license in case need to test with a key that expires
|
||||
// #warning FYI ROCKFISH is SET TO GENERATE A VERY SHORT TEST KEY
|
||||
// k.MaintenanceExpiration = k.LicenseExpiration = DateTime.UtcNow.AddMinutes(3); //TEST VALUE FOR DIAGNOSING LICENSE EXPIRATION ISSUES
|
||||
|
||||
//5k inside staff users will cover huge seeding level easily
|
||||
k.Features.Add(new LicenseFeature() { Feature = ACTIVE_INTERNAL_USERS_FEATURE_NAME, Count = 5000 });
|
||||
}
|
||||
else
|
||||
{
|
||||
//SUBSCRIPTION
|
||||
//trial period time limit
|
||||
k.MaintenanceExpiration = k.LicenseExpiration = DateTime.UtcNow.AddDays(TRIAL_PERIOD_DAYS);//NOTE: this preserves the current time, should it be set to midnight or something?
|
||||
|
||||
//20k customer contacts will cover huge seeding level easily
|
||||
//5k inside staff users will cover huge seeding level easily
|
||||
k.Features.Add(new LicenseFeature() { Feature = ACTIVE_INTERNAL_USERS_FEATURE_NAME, Count = 5000 });
|
||||
k.Features.Add(new LicenseFeature() { Feature = ACTIVE_CUSTOMER_USERS_FEATURE_NAME, Count = 20000 });
|
||||
k.Features.Add(new LicenseFeature() { Feature = MAXIMUM_DATA_GB_FEATURE_NAME, Count = 20 });
|
||||
}
|
||||
return GenerateRavenKey(k);
|
||||
}
|
||||
|
||||
|
||||
// //TESTING ONLY this is for development purposes only
|
||||
// //No external usage
|
||||
// public static string GetRavenTestKey(string dbid, bool Perpetual)
|
||||
// {
|
||||
// //Build a sample test key, sign it and return it
|
||||
// AyaNovaLicenseKey k = new AyaNovaLicenseKey();
|
||||
// k.LicenseFormat = "8";
|
||||
// k.RegisteredTo = "GZ TestCo Inc.";
|
||||
// k.DbId = dbid;
|
||||
// k.Perpetual = Perpetual;
|
||||
// if (Perpetual)
|
||||
// {
|
||||
// k.MaintenanceExpiration = k.LicenseExpiration = DateTime.UtcNow.AddDays(TRIAL_PERIOD_DAYS);
|
||||
// //5k inside staff users will cover huge seeding level easily
|
||||
// k.Features.Add(new LicenseFeature() { Feature = ACTIVE_INTERNAL_USERS_FEATURE_NAME, Count = 5000 });
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// //SUBSCRIPTION
|
||||
// k.MaintenanceExpiration = k.LicenseExpiration = DateTime.UtcNow.AddDays(TRIAL_PERIOD_DAYS);
|
||||
|
||||
// //20k customer contacts will cover huge seeding level easily
|
||||
// //5k inside staff users will cover huge seeding level easily
|
||||
// k.Features.Add(new LicenseFeature() { Feature = ACTIVE_INTERNAL_USERS_FEATURE_NAME, Count = 5000 });
|
||||
// k.Features.Add(new LicenseFeature() { Feature = ACTIVE_CUSTOMER_USERS_FEATURE_NAME, Count = 20000 });
|
||||
// k.Features.Add(new LicenseFeature() { Feature = MAXIMUM_DATA_GB_FEATURE_NAME, Count = 20 });
|
||||
|
||||
// }
|
||||
|
||||
// k.Features.Add(new LicenseFeature() { Feature = TRIAL_FEATURE_NAME, Count = 0 });
|
||||
// return GenerateRavenKey(k);
|
||||
// }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// RAVEN key generator
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal static string GenerateRavenKey(AyaNovaLicenseKey k)
|
||||
{
|
||||
|
||||
|
||||
if (string.IsNullOrWhiteSpace(k.RegisteredTo))
|
||||
throw new ArgumentException("RegisteredTo is required", "RegisteredTo");
|
||||
|
||||
// if (k.DbId == Guid.Empty)
|
||||
if (string.IsNullOrWhiteSpace(k.DbId))
|
||||
throw new ArgumentException("DBId is required", "RegisteredTo");
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
StringBuilder sbKey = new StringBuilder();
|
||||
StringWriter sw = new StringWriter(sbKey);
|
||||
|
||||
using (Newtonsoft.Json.JsonWriter w = new Newtonsoft.Json.JsonTextWriter(sw))
|
||||
{
|
||||
w.Formatting = Newtonsoft.Json.Formatting.Indented;
|
||||
|
||||
//outer object start
|
||||
w.WriteStartObject();
|
||||
|
||||
w.WritePropertyName("Key");
|
||||
|
||||
w.WriteStartObject();//start of key object
|
||||
|
||||
w.WritePropertyName("LicenseFormat");
|
||||
w.WriteValue(k.LicenseFormat);
|
||||
|
||||
w.WritePropertyName("Id");
|
||||
w.WriteValue(k.Id);
|
||||
|
||||
w.WritePropertyName("RegisteredTo");
|
||||
w.WriteValue(k.RegisteredTo);
|
||||
|
||||
w.WritePropertyName("DBID");
|
||||
w.WriteValue(k.DbId);
|
||||
|
||||
w.WritePropertyName("Perpetual");
|
||||
w.WriteValue(k.Perpetual);
|
||||
|
||||
w.WritePropertyName("LicenseExpiration");
|
||||
w.WriteValue(k.LicenseExpiration);
|
||||
|
||||
w.WritePropertyName("MaintenanceExpiration");
|
||||
w.WriteValue(k.MaintenanceExpiration);
|
||||
|
||||
|
||||
|
||||
//FEATURES
|
||||
// w.WritePropertyName("Features");
|
||||
// w.WriteStartObject();
|
||||
w.WritePropertyName("Features");
|
||||
w.WriteStartArray();
|
||||
|
||||
foreach (LicenseFeature lf in k.Features)
|
||||
{
|
||||
|
||||
w.WriteStartObject();
|
||||
|
||||
w.WritePropertyName("Name");
|
||||
w.WriteValue(lf.Feature);
|
||||
|
||||
if (lf.Count > 0)
|
||||
{
|
||||
w.WritePropertyName("Count");
|
||||
w.WriteValue(lf.Count);
|
||||
}
|
||||
|
||||
w.WriteEndObject();
|
||||
|
||||
}
|
||||
|
||||
|
||||
//end of features array
|
||||
w.WriteEnd();
|
||||
|
||||
//end of features object
|
||||
// w.WriteEndObject();
|
||||
|
||||
//end of AyaNova/AyaNovaLite key object
|
||||
w.WriteEndObject();
|
||||
|
||||
//close outer 'wrapper' object brace }
|
||||
w.WriteEndObject();
|
||||
|
||||
}//end of using statement
|
||||
|
||||
|
||||
// ## CALCULATE SIGNATURE
|
||||
|
||||
//GET JSON as a string with whitespace stripped outside of delimited strings
|
||||
//http://stackoverflow.com/questions/8913138/minify-indented-json-string-in-net
|
||||
string keyNoWS = System.Text.RegularExpressions.Regex.Replace(sbKey.ToString(), "(\"(?:[^\"\\\\]|\\\\.)*\")|\\s+", "$1");
|
||||
|
||||
|
||||
//**** Note this is our real 2016 private key
|
||||
var privatePEM = @"-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAz7wrvLDcKVMZ31HFGBnLWL08IodYIV5VJkKy1Z0n2snprhSi
|
||||
u3izxTyz+SLpftvKHJpky027ii7l/pL9Bo3JcjU5rKrxXavnE7TuYPjXn16dNLd0
|
||||
K/ERSU+pXLmUaVN0nUWuGuUMoGJMEXoulS6pJiG11yu3BM9fL2Nbj0C6a+UwzEHF
|
||||
mns3J/daZOb4gAzMUdJfh9OJ0+wRGzR8ZxyC99Na2gDmqYglUkSMjwLTL/HbgwF4
|
||||
OwmoQYJBcET0Wa6Gfb17SaF8XRBV5ZtpCsbStkthGeoXZkFriB9c1eFQLKpBYQo2
|
||||
DW3H1MPG2nAlQZLbkJj5cSh7/t1bRF08m6P+EQIDAQABAoIBAQCGvTpxLRXgB/Kk
|
||||
EtmQBEsMx9EVZEwZeKIqKuDsBP8wvf4/10ql5mhT6kehtK9WhSDW5J2z8DtQKZMs
|
||||
SBKuCZE77qH2CPp9E17SPWzQoRbaW/gDlWpYhgf8URs89XH5zxO4XtXKw/4omRlV
|
||||
zLYiNR2pifv0EHqpOAg5KGzewdEo4VgXgtRWpHZLMpH2Q0/5ZIKMhstI6vFHP1p7
|
||||
jmU4YI6uxiu7rVrZDmIUsAGoTdMabNqK/N8hKaoBiIto0Jn1ck26g+emLg8m160y
|
||||
Xciu5yFUU+PP1SJMUs+k1UnAWf4p46X9jRLQCBRue9o0Ntiq/75aljRoDvgdwDsR
|
||||
mg4ZANqxAoGBAPBoM5KoMZ4sv8ZFv8V+V8hgL5xiLgGoiwQl91mRsHRM/NQU5A/w
|
||||
tH8nmwUrJOrksV7kX9228smKmoliTptyGGyi1NPmSkA7cN9YYnENoOEBHCVNK9vh
|
||||
P+bkbMYUDNMW4fgOj09oXtQtMl5E2B3OTGoNwZ2w13YQJ8RIniLPsX7nAoGBAN01
|
||||
eQNcUzQk9YrFGTznOs8udDLBfigDxaNnawvPueulJdBy6ZXDDrKmkQQA7xxl8YPr
|
||||
dNtBq2lOgnb6+smC15TaAfV/fb8BLmkSwdn4Fy0FApIXIEOnLq+wjkte98nuezl8
|
||||
9KXDzaqNI9hPuk2i36tJuLLMH8hzldveWbWjSlRHAoGBAKRPE7CQtBjfjNL+qOta
|
||||
RrT0yJWhpMANabYUHNJi+K8ET2jEPnuGkFa3wwPtUPYaCABLJhprB9Unnid3wTIM
|
||||
8RSO1ddd9jGgbqy3w9Bw+BvQnmQAMpG9iedNB+r5mSpM4XSgvuIO+4EYwuwbMXpt
|
||||
nVx+um4Eh75xnDxTRYGVYkLRAoGAaZVpUlpR+HSfooHbPv+bSWKB4ewLPCw4vHrT
|
||||
VErtEfW8q9b9eRcmP81TMFcFykc6VN4g47pfh58KlKHM7DwAjDLWdohIy89TiKGE
|
||||
V3acEUfv5y0UoFX+6ara8Ey+9upWdKUY3Lotw3ckoc3EPeQ84DQK7YSSswnAgLaL
|
||||
mS/8fWcCgYBjRefVbEep161d2DGruk4X7eNI9TFJ278h6ydW5kK9aTJuxkrtKIp4
|
||||
CYf6emoB4mLXFPvAmnsalkhN2iB29hUZCXXSUjpKZrpijL54Wdu2S6ynm7aT97NF
|
||||
oArP0E2Vbow3JMxq/oeXmHbrLMLQfYyXwFmciLFigOtkd45bfHdrbA==
|
||||
-----END RSA PRIVATE KEY-----";
|
||||
|
||||
PemReader pr = new PemReader(new StringReader(privatePEM));
|
||||
AsymmetricCipherKeyPair keys = (AsymmetricCipherKeyPair)pr.ReadObject();
|
||||
var encoder = new UTF8Encoding(false, true);
|
||||
var inputData = encoder.GetBytes(keyNoWS);
|
||||
var signer = SignerUtilities.GetSigner("SHA256WITHRSA");
|
||||
signer.Init(true, keys.Private);
|
||||
signer.BlockUpdate(inputData, 0, inputData.Length);
|
||||
var sign = signer.GenerateSignature();
|
||||
var signature = Convert.ToBase64String(sign);
|
||||
|
||||
|
||||
System.Text.StringBuilder sbOut = new StringBuilder();
|
||||
sbOut.AppendLine("[KEY");
|
||||
sbOut.AppendLine(sbKey.ToString());
|
||||
sbOut.AppendLine("KEY]");
|
||||
sbOut.AppendLine("[SIGNATURE");
|
||||
sbOut.AppendLine(signature);
|
||||
sbOut.AppendLine("SIGNATURE]");
|
||||
|
||||
|
||||
return sbOut.ToString();
|
||||
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//eoc
|
||||
}
|
||||
//eons
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Sockeye.Util
|
||||
{
|
||||
@@ -138,6 +139,32 @@ namespace Sockeye.Util
|
||||
}
|
||||
|
||||
|
||||
public static string ToHex(string str)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var bytes = Encoding.ASCII.GetBytes(str);
|
||||
foreach (var t in bytes)
|
||||
{
|
||||
sb.Append(t.ToString("X2"));
|
||||
}
|
||||
|
||||
return sb.ToString(); // returns: "48656C6C6F20776F726C64" for "Hello world"
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static string FromHex(string hexString)
|
||||
{
|
||||
var bytes = new byte[hexString.Length / 2];
|
||||
for (var i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
bytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
|
||||
}
|
||||
|
||||
return Encoding.ASCII.GetString(bytes); // returns: "Hello world" for "48656C6C6F20776F726C64"
|
||||
}
|
||||
|
||||
}//eoc
|
||||
|
||||
}//eons
|
||||
Reference in New Issue
Block a user