[连载]《C#通讯(串口和网络)框架的设计与实现》- 14.序列号的设计,不重复的实现一机一码

目       录

第十四章     序列号的设计... 2

14.1        设计原则... 2

14.2        设计思想... 3

14.3        代码实现... 4

14.4        代码混淆... 18

14.5        代码破解... 18

14.6        小结... 18

 

第十四章      序列号的设计

    序列号作为软件使用授权方式之一,被广泛使用在应用软件方面。主要考虑到这几方面:1.对知识产权的保护,毕竟付出来脑力劳动和体力劳动。2.商业竞争中增加防守的能力,防止被竞争对手盗取。3.增强合同的执行效力,防止另一方由于各种原因破坏合作机制。

    基于上述方面,从保护、防守思维模式角度考虑,增加序列号功能是有必要的。每个作者或公司设计序列号的方式不一样,就是因为不一样,所以才能达到我们增加该功能的效果。

14.1     设计原则

  1. 序列号长度尽可能短

    主要是从成本角度考虑的。例如用户现场需要一个正版软件的序列号,你把序列号信息通过什么方式传递给用户呢?假设我们用对称或非对称方式生成一个很长的序列号,如果口述告诉对方的话,那么对方肯定要用纸和笔进行记录,最后输入到软件后还不一定正确;如果把序列号以文件的方式通过网络传递给对方,那么需要占用网络资源,另外对方的电脑不一定有网络环境。不管如何,很长的序列号在生成和传递的过程中可能涉及到的成本包括:材料成本、流量成本、人力成本和时间成本等。

     如果一个字符可以表达序列号所需要的完整信息,那么是最理想的。但是,这是理想状态,是不可能实现的,至少以我现在的能力是无法完成的。所以,要以最佳的长度表达出序列号的全部信息。

  1. 避免出现容易混淆的字符生成一个序列号发给了用户,这个序列号包括:数字0和字母O,数字1和字母l。难道让用户一遍一遍的试嘛,这样的用户体验太差了,虽然嘴上不说出来,至少感觉不太舒服。

14.2     设计思想

    设计的思想要看序列号要实现什么样的功能和具备什么属性。从功能角度考虑,包括:1.一个计算机一个序列号;2.尽管输入的条件都一样,每次生成的序列号都不一样;3.对使用的时限进行验证;4.序列号有注册时限,超过规定的使用时间,序列号作废,避免短时间多次注册。从属性角度考虑,包括:同样的计算机、同样的输入条件生成的序列号都不一样。

   我们把上述因素考虑进去,序列号长度为25位字符,序列号生成格式和元素信息如下图:

 [连载]《C#通讯(串口和网络)框架的设计与实现》- 14.序列号的设计,不重复的实现一机一码

 X01-X05:为计算机的特征码,5位字符串,获得机器某个部件的ID,这个部件可能为CPU、网卡、硬盘等信息,把ID进行MD5加密后取前5个字符作为特征码,来实现一机一码。这种方式,特征码有可能有相同的情况,但是机率很小。

 X06-X13:为生成序列号的日期,8位字符串,格式为:yyyyMMdd。与后边的使用时间限制配合使用,来验证软件的使用期限。

 X14-X15:为注册时间限制,2位数字字符,从生成序列号日期算起,超过此注册时间限制,序列号将无法正常进行注册操作。

 X16-X20:为使用时间限制,5位数字字符,与生成序列号日期配合使用来验证软件使用期限。

 X21:为序列号的偏移量,1位字符,不管在什么场景下,每次生成序列号的偏移量都不一样。

X22-X25:为保留数据位,暂时不使用。自定义一个序列号字典信息,例如:_Dictionary ="JCB8EF2GH7K6MVP9QR3TXWY4",把容易混淆的字符去掉,这个可以自定义。序列号的每个部分都是通过随机生成的偏移量(X21),对字典进行位移,根据输入的数字信息对应字典的下标提取相应的字符作为序列号的一个字符。

   生成序列号的大概过程:

  1. 在字典信息的长度范围内随机生成一个偏移量数字。
  2. 根据偏移量数字对字典进行左或右的循环移动。
  3. 根据输入的数字信息,例如:2015中的2,作为下标,从字典信息中提取出相应的字符。

反向解析大概过程类似,只需要根据X21字符,与字典的字符进行匹配,对应的下标作为偏移量,就可以反向解析出各项信息。

14.3     代码实现

1.MD5操作类:

public class Safety
{
       public static string MD5(string str)
       {
              string strResult = "";
              MD5 md5 = System.Security.Cryptography.MD5.Create();
              byte[] bData = md5.ComputeHash(Encoding.Unicode.GetBytes(str));
              for (int i = 0; i < bData.Length; i++)
              {
                     strResult = strResult + bData[i].ToString("X");
              }
              return strResult;
       }
}

2.注册信息类:

public class RegInfo
{
       public RegInfo()
       {
              KeySn = "";
              Date=DateTime.MinValue;
              RegLimitDays = 0;
              UseLimitDays = 0;
              Offset = 0;
       }
       public string KeySn { get; set; }
       public DateTime Date { get; set; }
       public int RegLimitDays { get; set; }
       public int UseLimitDays { get; set; }
       public int Offset { get; set; }
}

3.偏移操作类型:

internal enum OffsetType
{
       Left,
       Right
}

4.        序列号管理类

public class LicenseManage
    {
        /// <summary>
        /// 序列号字典,把数字和字母容易混淆的字符去掉。所产生的25位序列号从这个字典中产生。
        /// </summary>
        private static string _Dictionary = "JCB8EF2GH7K6MVP9QR3TXWY4";

        /// <summary>
        /// 可以自定义字典字符串
        /// </summary>
        public static string Dictionary
        {
            get { return _Dictionary; }
            set
            {
                if (value.Length < 9)
                {
                    throw new ArgumentOutOfRangeException("设置的字典长度不能小于9个字符");
                }

                _Dictionary = value;
            }
        }

        /// <summary>
        /// 生成序列号
        /// </summary>
        /// <param name="key">关键字,一般为CPU号、硬盘号、网卡号,用于与序列号绑定,实现一机一码</param>
        /// <param name="now">现在的时间</param>
        /// <param name="regLimitDays">注册天数限制,超过此天数,再进行注册,序列号就失效了,不能再使用了</param>
        /// <param name="useLimitDays">使用天数限制,超过此天数,可以设置软件停止运行等操作</param>
        /// <returns>返回序列号,例如:xxxxx-xxxxx-xxxxx-xxxxx-xxxxx</returns>
        public static string BuildSn(string key, DateTime now, int regLimitDays, int useLimitDays)
        {
            if (regLimitDays < 0 || regLimitDays > 9)
            {
                throw new ArgumentOutOfRangeException("注册天数限制范围为0-9");
            }

            if (useLimitDays < 0 || useLimitDays > 99999)
            {
                throw new ArgumentOutOfRangeException("使用天数限制范围为0-99999");
            }

            /*
             *关键字用MD5加密后,取后5个字符作为序列号第1组字符
             */
            string md5 = Safety.MD5(key);
            string x1 = md5.Substring(md5.Length - 5);

            /*
             * 生成随机偏移量
             */
            Random rand = new Random();
            int offset = rand.Next(1, Dictionary.Length - 1);

            /*
             * 第5组的第1个字符保存偏移量字符,其余4个字符随机生成,作为保留位
             */
            string x5 = Dictionary[offset].ToString();
            for (int i = 0; i < 4; i++)
            {
                x5 += Dictionary[rand.Next(0, Dictionary.Length - 1)].ToString();
            }

            /*
             * 以注册时间(yyyyMMdd)和注册时间限制生成第2组和第3组序列号,一共10位字符串
             */
            string dateSn = GetDateSn(now, offset);
            string regLimitSn = GetRegLimitSn(regLimitDays, offset);
            string x2 = dateSn.Substring(0, 5);
            string x3 = dateSn.Substring(dateSn.Length - 3) + regLimitSn;

            /*
             *以使用时间限制生成第4组序列号,一共5位字符串
             */
            string x4 = GetUseLimitSn(useLimitDays, offset);

            return String.Format("{0}-{1}-{2}-{3}-{4}", x1, x2, x3, x4, x5);
        }

        /// <summary>
        /// 注册序列号
        /// </summary>
        /// <param name="key">关键字,一般为CPU号、硬盘号、网卡号,用于与序列号绑定,实现一机一码</param>
        /// <param name="sn">序列号</param>
        /// <param name="desc">描述信息</param>
        /// <returns>注册状态,成功:0</returns>
        internal static int RegSn(string key, string sn, ref string desc)
        {
            if (String.IsNullOrEmpty(key) || String.IsNullOrEmpty(sn))
            {
                throw new ArgumentNullException("参数为空");
            }

            LicenseInfo regInfo = GetRegInfo(sn);
            string md5 = Safety.MD5(key);
            if (String.CompareOrdinal(md5.Substring(md5.Length - 5), regInfo.KeySn) != 0)
            {
                desc = "关键字与序列号不匹配";
                return -1;//关键字与序列号不匹配
            }

            if (regInfo.Date == DateTime.MaxValue || regInfo.Date == DateTime.MinValue || regInfo.Date > DateTime.Now.Date)
            {
                desc = "序列号时间有错误";
                return -2;//序列号时间有错误
            }

            TimeSpan ts = DateTime.Now.Date - regInfo.Date;
            if (ts.TotalDays > 9 || ts.TotalDays < 0)
            {
                desc = "序列号失效";
                return -3;//序列号失效
            }

            if (regInfo.UseLimitDays <= 0)
            {
                desc = "使用期限受限";
                return -4;//使用期限受限
            }

            Application.UserAppDataRegistry.SetValue("SN", sn);

            desc = "注册成功";
            return 0;
        }

        /// <summary>
        /// 检测序列号,试用于时钟定时调用
        /// </summary>
        /// <param name="key">关键字,一般为CPU号、硬盘号、网卡号,用于与序列号绑定,实现一机一码</param>
        /// <param name="desc">描述信息</param>
        /// <returns>检测状态,成功:0</returns>
        internal static int CheckSn(string key, ref string desc)
        {
            if (String.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("参数为空");
            }

            object val = Application.UserAppDataRegistry.GetValue("SN");

            if (val == null)
            {
                desc = "未检测到本机的序列号";
                return -1;
            }

            string sn = val.ToString();

            LicenseInfo regInfo = GetRegInfo(sn);
            string md5 = Safety.MD5(key);
            if (String.CompareOrdinal(md5.Substring(md5.Length - 5), regInfo.KeySn) != 0)
            {
                desc = "关键字与序列号不匹配";
                return -2;//关键字与序列号不匹配
            }

            if ((DateTime.Now.Date - regInfo.Date).TotalDays > regInfo.UseLimitDays)
            {
                desc = "序列使用到期";
                return -3;//关键字与序列号不匹配
            }

            desc = "序列号可用";
            return 0;
        }

        /// <summary>
        /// 获得剩余天数
        /// </summary>
        /// <param name="key">关键字,一般为CPU号、硬盘号、网卡号,用于与序列号绑定,实现一机一码</param>
        /// <returns>剩余天数</returns>
        internal static int GetRemainDays(string key)
        {
            if (String.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("参数为空");
            }

            object val = Application.UserAppDataRegistry.GetValue("SN");

            if (val == null)
            {
                return 0;
            }

            string sn = val.ToString();

            LicenseInfo regInfo = GetRegInfo(sn);
            string md5 = Safety.MD5(key);
            if (String.CompareOrdinal(md5.Substring(md5.Length - 5), regInfo.KeySn) != 0)
            {
                return 0;//关键字与序列号不匹配,不能使用。
            }

            //<=0的情况,证明不可以使用。
            return regInfo.UseLimitDays - (int)(DateTime.Now.Date - regInfo.Date).TotalDays;
        }

        /// <summary>
        /// 根据序列号,反推注册信息
        /// </summary>
        /// <param name="sn">序列号</param>
        /// <returns>注册信息</returns>
        private static LicenseInfo GetRegInfo(string sn)
        {
            LicenseInfo reg = new LicenseInfo();
            string[] splitSn = sn.Split('-');
            if (splitSn.Length != 5)
            {
                throw new FormatException("序列号格式错误,应该带有'-'字符");
            }

            reg.KeySn = splitSn[0];
            reg.Offset = Dictionary.IndexOf(splitSn[4][0]);
            reg.Date = GetDate(splitSn[1] + splitSn[2].Substring(0, 3), reg.Offset);
            reg.RegLimitDays = GetRegLimitDays(splitSn[2].Substring(3, 2), reg.Offset);
            reg.UseLimitDays = GetUseLimitDays(splitSn[3], reg.Offset);
            return reg;
        }

        /// <summary>
        /// 以当前时间和偏移量生成当前时间对应的字符串
        /// </summary>
        /// <param name="now">当前时间</param>
        /// <param name="offset">偏移量</param>
        /// <returns>返回日期对应的字符串,8位字符串</returns>
        private static string GetDateSn(DateTime now, int offset)
        {
            string dateSn = "";
            string date = now.ToString("yyyyMMdd");
            string newDic = Dictionary;
            for (int i = 0; i < date.Length; i++)
            {
                newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
                int num = int.Parse(date[i].ToString());
                dateSn += newDic[num].ToString();
            }
            return dateSn;
        }

        /// <summary>
        /// 根据注册时间序列号反推注册时间
        /// </summary>
        /// <param name="dateSn">时间字符串</param>
        /// <param name="offset">偏移量</param>
        /// <returns>时间</returns>
        private static DateTime GetDate(string dateSn, int offset)
        {
            string dateStr = "";
            string newDic = Dictionary;
            for (int i = 0; i < dateSn.Length; i++)
            {
                newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
                int num = newDic.IndexOf(dateSn[i]);
                dateStr += num;
            }
            return new DateTime(int.Parse(dateStr.Substring(0, 4)), int.Parse(dateStr.Substring(4, 2)), int.Parse(dateStr.Substring(6, 2)));
        }

        /// <summary>
        /// 以注册时间限制和偏移量生成对应的字符串
        /// </summary>
        /// <param name="regLimitDays"></param>
        /// <param name="offset"></param>
        /// <returns>返回对应的注册时间限制的字符串,2位字符串</returns>
        private static string GetRegLimitSn(int regLimitDays, int offset)
        {
            string regLimitSn = "";
            string regLimitStr = regLimitDays.ToString("00");
            string newDic = Dictionary;
            for (int i = 0; i < regLimitStr.Length; i++)
            {
                newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
                int num = int.Parse(regLimitStr[i].ToString());
                regLimitSn += newDic[num].ToString();
            }
            return regLimitSn;
        }

        /// <summary>
        /// 根据注册时间限制字符串反推注册时间限制
        /// </summary>
        /// <param name="regLimitSn">注册时间限制字符串</param>
        /// <param name="offset">偏移量</param>
        /// <returns>注册时间限制</returns>
        private static int GetRegLimitDays(string regLimitSn, int offset)
        {
            string regLimitStr = "";
            string newDic = Dictionary;
            for (int i = 0; i < regLimitSn.Length; i++)
            {
                newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
                int num = newDic.IndexOf(regLimitSn[i]);
                regLimitStr += num;
            }
            return int.Parse(regLimitStr);
        }

        /// <summary>
        /// 以使用时间限制和偏移量生成对应的字符串
        /// </summary>
        /// <param name="useLimitDays">使用时间限制</param>
        /// <param name="offset">偏移量</param>
        /// <returns>使用时间限制对应字符串,5位字符串</returns>
        private static string GetUseLimitSn(int useLimitDays, int offset)
        {
            string useLimitSn = "";
            string useLimitStr = useLimitDays.ToString("00000");
            string newDic = Dictionary;
            for (int i = 0; i < useLimitStr.Length; i++)
            {
                newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
                int num = int.Parse(useLimitStr[i].ToString());
                useLimitSn += newDic[num].ToString();
            }
            return useLimitSn;
        }

        /// <summary>
        /// 根据使用时间限制字符串反推使用时间限制
        /// </summary>
        /// <param name="regLimitSn">使用时间限制字符串</param>
        /// <param name="offset">偏移量</param>
        /// <returns>使用时间限制</returns>
        private static int GetUseLimitDays(string useLimitSn, int offset)
        {
            string useLimitStr = "";
            string newDic = Dictionary;
            for (int i = 0; i < useLimitSn.Length; i++)
            {
                newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
                int num = newDic.IndexOf(useLimitSn[i]);
                useLimitStr += num;
            }
            return int.Parse(useLimitStr);
        }

        /// <summary>
        /// 根据字典、偏移量和偏移类型生成新的字典
        /// </summary>
        /// <param name="dic"></param>
        /// <param name="offset"></param>
        /// <param name="offsetType"></param>
        /// <returns></returns>
        private static string GetNewDictionaryString(string dic, int offset, LicenseOffset offsetType)
        {
            StringBuilder sb = new StringBuilder(dic);
            if (offsetType == LicenseOffset.Left)
            {
                for (int i = 0; i < offset; i++)
                {
                    string head = sb[0].ToString();
                    sb.Remove(0, 1);
                    sb.Append(head);
                }
            }
            else if (offsetType == LicenseOffset.Right)
            {
                for (int i = 0; i < offset; i++)
                {
                    string end = sb[dic.Length - 1].ToString();
                    sb.Remove(dic.Length - 1, 1);
                    sb.Insert(0, end);
                }
            }
            return sb.ToString();
        }
    }

 

14.4     代码混淆

   从安全角度来讲,.NET程序如果不加混淆的话,很容易被反编译出源代码的。从专业角度来讲,即使增加了序列号功能,也无济于事,专业的人员分分钟可以破解掉,尽管这样干的人很少,但是存在这种可能性。如果一个软件人员想了解一个很好的软件,第一反映可能就是反编译。

   对于公司或商业使用的软件来讲,增加混淆还是有必要的,尽管现在开源很流行。

14.5     代码破解

    不管.NET程序如何进行混淆,理论上都是可以破解的,理论的东西就不赘述了。通常接触过的破解方式有两种:注册机方式和暴力方式。

    注册机的方式,需要通过软件的验证序列号的过程和机制反向推算出序列号的生成算法,根据反推的算法开发一个小软件,用于生成脱离作者授权生成序列号。这种方式不会破坏程序本身的代码,相对温和。暴力的方式,就是找到序列号验证部分的代码,通过删除或绕过验证代码等方式不让代码有效执行。这种方式会对程序本身的代码进行改动,所以也存在一些风险。

14.6     小结

     实现序列号有多种方式,上述方式不一定最好,但是希望对开发者有一定帮助。

 

最终实现效果图如下:

[连载]《C#通讯(串口和网络)框架的设计与实现》- 14.序列号的设计,不重复的实现一机一码

作者:唯笑志在

Email:504547114@qq.com

QQ:504547114

.NET开发技术联盟:54256083

文档下载:http://pan.baidu.com/s/1pJ7lZWf

官方网址:http://www.bmpj.net

上一篇:如何选择云主机


下一篇:word2007插件开发经验备忘2--如何操作word