在2021年生成加密安全的许可证密钥

2021-06-04 01:38:02

生成和验证许可证密钥是许多商业软件软件天数的常见要求。从桌面应用程序,如框架内置的框架Qt等框架,到双许可的开源包和库,如sidekiq,以及各种其他内部内部软件应用程序和依赖项。

但作为一个独立的软件供应商(ISV),您可能会问自己 - "什么技术在2021年选择软件许可?"

好吧,在我们到达答案之前,我们需要了解一些历史。

谈到软件许可时,密钥生成和验证算法vendorschoose可以制作或打破许可系统。在验证算法后,Vendorcan不再信任任何先前生成的许可证密钥,包括属于合法用户的许可证密钥。

移动到在线许可服务器,在那里我们可以检查键对针对有效键列表的键。如果您选择构建内部许可系统,这可能会令人难以置信的昂贵,两者在长期和长期。这也可以限制我们的应用程序' S许可证Checksto在线,这可能少于理想。

移动到现代许可证密钥算法。此解决方案具有使所有先前生成的许可证密钥无效的相当不幸的副作用,包括属于具有迁移计划的合法用户的许可证密钥。

这两个解决方案都可以以最终用户信任,支持成本以及工程资源而言,这两者都可以实现巨大的成本。足以说,它'是一个糟糕的情况。

理想情况下,我们要做的是通过从Get-Go中选择现代的SecureLicense密钥算法来完全避免这些情况。

但在我们介绍如何,让我们快速查看一些最大的攻击Vectors许可,这可能有助于我们了解如何捍卫他们。

软件"开裂"是直接修改软件应用程序的源代码的行为完全绕过其许可系统。与供应商讨厌听到它一样:所有在最终用户设备上都能易于破解。

(毕竟,应用程序只是由位于本地存储的位和字节,一个字节,Zeroes,无论供应商如何尝试如何努力,都可以始终修改这些。)

软件裂缝通常仅为单个版本的特定应用程序工作,真诚的应用程序代码本身被修改为绕过任何许可证检查(意味着软件扫描通常需要更新的新应用程序代码的更新裂缝。)分配扫描的应用程序掉落糟糕的演员。

另一个主要攻击矢量被称为软件" Keygen",这更不祥。 ASITS名称可能意味着,keygen是一种软件的形式,通常是一个单独的程序或网页,即,现成有效的许可证键,即键生成器,或" keygen。"

大多数软件供应商都有某种类型的许可证键,他们保持秘密。例如,随后用户提交了成功的采购订单,部分订单流程调用了一个密钥生成器,即为新客户提供有效的合法许可密钥。

但是,当涉及易受攻击的遗产许可证密钥算法时,一个糟糕的演员也可能能够进行类似的壮举 - 生成有效的,虽然非法,许可证密钥,但授予他们普及一些努力反向工程师算法。

根据您的密钥生成算法,像这样的keygen可以只能为应用程序的单个版本生成有效性。但在最坏的情况下,一个坏演员可以创建keygenthat生成跨所有版本的有效许可证密钥,要求产品的完全动态系统和#39; S许可系统。

它也值得一提的是,对于裂缝而言,keygens的keygens也比裂缝更有价值,因此可以在实际应用程序上使用,而是不必分发修改,破解版本的应用程序的错误演员。

现在,我们暗示了这种遗产算法,它实际上仍然在这一天通过软件供应商的数量使用。它' s称为部分键验证,虽然它看起来可能似乎是一个良好的系统,但它是通过默默无闻的安全性。

如今,写出部分密钥验证(PKV)算法实际上比简单地是正确的方式更多的工作。但是为了理解,让我们写下自己的部分钥匙术系统。然后我们'重新打破它。

部分键验证是将产品密钥分区为多个&#34的软件许可证密钥算法;子键。"使用每个新版本的产品,您的许可证密钥验证算法将检查许可证的差异化' s子ke 。

它' s称为部分密钥验证,因为验证算法永远不会测试Full许可证密钥,它只测试子字母的子集。 (或者他们说。)

我建议在2007年从布兰登读上面的博客文章,他的实施在Delphi中的实施。但如果你'重新进入delphi,我们' ll将他的部分键验证算法移植到节点。

PKV键的主要组成部分是种子值及其子keys(一起参考为序列),然后是校验和。子键源自唯一的种子值,使用位绑定完成,以及校验查找,以确保串行(种子+子键)不包含拼写错误。 (是的...在Oldendays中,一个人实际上必须用手输入许可证键。)

我们'重新进入每个组件的细节,例如,校验组织如何,因为布兰登'帖子详细介绍了所有这些。

与那个说,让' s承担了一个关于发布新应用程序的业务的角色.we'重新编写一个keygen,我们可以用来生成合法键FORUT最终用户他们购买产品后。

我们的PKV KEYGEN应该是一个紧紧的商业秘密,因为它是在愿意制作钥匙的力量。但我们很快就会实现我们的消亡,实际上不可能实现PKV Keygen Secretis。

const crypto =要求(' crypto')//格式化一个数字到固定长度的heprifimal stringfunction tofixedhex(num,len){return num.tostring(16).touppercase()。padstart(len,& #39; 0')。子字符串(0,LEN)} //从种子(A,B,C为位Twiddling的参数)函数GetSubKeyed(Seed,A,B,C){if( typeof seew ===' string'){seed = parseint(种子,16)} a = a%25 b = b%3,如果(a%2 === 0){subkey =( (种子>> a)& 0x000000ff)^((种子> b)| c)& 0xFF} else {subkey =((种子> a)& 0x000000ff)^((种子> b)& c)& 0xff}返回tofixedhex(subkey,2)}} //获取给定串行stringfunction getChecksumforserial(串行){let右= 0x00af // 175的校验和左侧= 0x0056 // 101 for(var i = 0; i<串行.Length; i ++){右+ = serial.CharCodeat(i)if(右和gt; 0x00FF){右 - = 0x00FF}左+ =右如果(left> 0x00ff){left-= 0x00ff}} return tofixedhex((左<< 8)+右,4)} //格式化键(xxxx-xxxx-xxxx-xxxx-xxxx)函数formatekey(key){return key.match(/。{4} / g).join (' - ')} //生成4字节的heprimimal种子valutipyfunction(n){const seed = crypto.randombytes(4).tostring(' hex')返回种子.percase()} //生成(合法)许可证键函数生成(Seed){//构建子项列表(位绑定参数是任意的,但永远不会改变)const subkeys = [getsubkeyfromseed(seed,24,3,200 ),getsubkeyfomseed(种子,10,0,56),getsubkeyfomseed(种子,1,2,91),getsubkeyfomseed(种子,7,100),] //构建串行(种子+子键)const序列= seed + seed + searkeys.join('')//构建键(串行+校验和)const键=串行+ getChecksumforserial(串行)返回formareykey(key)}

是的 - 它'很多很多。大多数读者都赢得了' t对所有杀死的数字和漂亮的比特扣舒服。 (并正确地 - 它是令人困惑的,Evento Me,因为我在Delphi代码上港口并写这篇文章。)

接下来,让'分解这个新密钥,ECE4-4EDB-37E8-7FF9-BC96。让'点击关键的组成部分:种子,子键和校验和。

现在,用于生产使用的keygen可以具有更多的子键,或者子键可以被排列不同地混合,但算法仍然是或多或少的统称。算法和漏洞的算法。

好吧,让'请记住," p"在" pkv"代表"部分" - 部分密钥验证。我们许可证密钥验证算法应该只一次验证一个子键,我们可以根据需要旋转或每个版本的应用程序。

函数ISKeyFormatValid(key){return key.length === 24&& key.replace(/ - / g,'')。length === 20}函数isseeyformatvalid(seed){return seed.match(/ [a-f0-9] {8} /)! = null}函数isererialchecksumvalid(串行,校验和){const c = getchecksumforserial(串行)return c === checksum}函数是key iskeyvalid(key){if(!iskeyformatvalid(key)){return false} const [,串行,校验和] = key.replace(/ - / g,'')。匹配(/(。{16})(。{4})/)如果(!isererialchecksumvalid(串行,校验和)){返回false const seed = serial.substring(0,8)如果(!isseeDformatvalid(Seed)){return false} //验证0th子ketcent期望= getsubkeyfromseed(Seed,24,3,200)const实际= serial.substring(8 ,10)如果(实际!==预期){return false}返回true}

验证算法的主旨是我们首先检查键格式化,然后' llverify校验和有效。接下来,我们' ll验证种子值的格式,如果werecall是串行的前8个字符。

//验证0th subkeyconst预期= getsubkeyfromseed(Seed,24,3,200)const实际= serial.substring(8,10)(实际!==预期){return false}

如果您注意到,GetSubKeyFremseed(Seed,24,3,200)正在从这些值中获取预期的0次子。然后,我们将预期的第0个子键与我们的许可证密钥进行比较。如果TheSubKeys Don' t匹配,许可证密钥无效。

但是,如果我们'重新包括精确的0th子键参数,它由我们的秘密keygen,Inour应用程序代码,ISN' t那坏?绝对地!这就是我们打破PKV的方式。因此,我们一直在等待 - 我们一直在等待 - 写一个'非法' keygen。

假设'担心糟糕的演员的角色。并让'审查我们所知道的到目前为止:

Let' S仅使用验证码中包含的操作,即,具有有效的0th子项的许可键。

Const Seed =' 00000000' const subkey = getsubkeyfseed(seed,24,3,200)const serial =`$ {seed} $ {subkey} 000000`const checksum = getChecksumforserial(串行)const键=`$ $ {checksum}`.match(/。{4} / g).join(' - ')console.log({key})// => {键:' 0000-0000-C800-0000-BBCD' }

那个'很多零。此外,未归零的唯一组件是0thsubkey和校验和。但这可以' t可能是一个有效的钥匙,对吗?

好吧,'不好。好吧,实际上......它'对于'我们',糟糕的演员;对于企业申请不好,我们刚刚为此写了一个工作键。我们只需要递增heprimalseed值以生成更多有效的许可证密钥:

- Const Seed =' 00000000' + Const Seed =' 00000001' const subkey = getsubkeyfremseed(seed,24,3,200)const serial =`$ {seed} $ {subkey} 000000 `const checksum = getChecksumforserial(串行)const key = formatey(`$ {serial} $ {checksum}`)

嗯,那个' s双倍不好,为他们。作为墨菲'法律将预测,这个keygen已提交给一个受欢迎的在线留言板,业务没有控制。重点在普及,销售逢低,利益相关者不开心。

让'再次担任业务的作用。我们需要解决这个问题。幸运的是,我们的Chosenkey算法让我们从验证第0个子核,验证第一个子项。 Allwe必须做的是调整子项参数:

- //验证0th子kanc-const预期= getsubkeyfromseed(Seed,24,3,200) - const实际= serial.substring(8,10)+ //验证第1个子+ const预期= getsubkeyfromseed(种子,10,0,56 )+ const实际= serial.substring(10,12)如果(实际!==预期){return false}

让'迅速使这一变化并推出​​一个沉默的更新来限制任何未来的Damagethis糟糕的演员可能会造成。幸运的是,我们的应用程序自动更新,所以这应该是一个快速修复。

让'将我们的角色作为糟糕的演员。我们的keygen的用户声称它没有长度的工作,这很奇怪,因为它以前最肯定的工作。他们'重新支付usin加密货币,甚至虽然我们'再是一个坏人,我们喜欢让客户快乐。

我们注意:已更改的第一个变量是应用程序似乎堆叠本身。在新版本挖掘后,我们重新评估这种情况。

所有'他们'从验证第0个子键到第一个子项的移动了。让' s调整库keygen程序,使其再次生成有效的产品密钥:

Const Seed =' 00000000' - const subkey = getsubkeyfoseed(Seed,24,3,200) - const serial =`$ {seed} $ {subkey} 000000` + const subkey = getsubkeyfremseed(种子,10, 0,56)+ const erial =`$ {seed} 00 $ {subkey} 0000`const checksum = getChecksumforserial(串行)const键=`$ {serial} $ {checksum}`.match(/。{4} / g ).join(' - ')

作为使用PKV的业务,我们可以继续调整我们的第n个子核查,因为它们弹出它们时要对抗这些键合。我们有警报设置各种指示符关键词和所有内容。

但是有一个主要问题。 (现在可能不是问题,但很快就会。)

它简单:一旦我们开始验证第二个子键,错误的演员将再写一个keygen,然后是第3个子键,我们'如果我们使用100个子短块耗尽是不可避免的。

这意味着在我们' ve通过验证我们的每个子key,在我们聪明的尝试中致力于打击keygens,我们很快就不再追索了。当然,我们可以直接在我们的申请代码中开始黑名单的种子,但是那个傻瓜'在那里的差事' s的差错而不是小的子麦克斯。

嗯,在这种情况结束时,一旦所有子键参数都泄露,Bad Actorcan完全复制了我们的秘密键! (毕竟,我们' vere字面上给了他们对乌斯堡的钥匙。这是一个缓慢的涓涓细流,但他们是耐心的。)

要坦率,部分关键验证是很多工作,特别是对于我们最终全部泄露的关键算法。 PKV因其本质而缺乏。当然,莫斯巴克斯有,泄漏整个算法的时间越长。也许那个' s一件事。也许是糟糕的演员isn' t复杂,以保持子项参数的记录。

一些应用程序将在媒体码中具有一个中心点,其中通过内联通过联系许可证密钥检查来实现其系统,使得一个糟糕的演员希望破解软件的工作,更加困难。但许可都基本上是一样的:它'条件的系列。

考虑到这一点,有没有利益使用PKV,一个许可方案,将其倾向于将其秘密的许可方案展开,这是一个糟糕的演员,与现代加密。它' s不是更安全的,它'不容易分发,它不会保护您免受keygens。 PKV是通过设计,通过默默无闻的安全。它不应再使用它。

在选择现代许可证密钥算法时,我们有一个相当多的实体选项。例如,OURAPI支持许可证密钥的各种密码方案,从椭圆曲线签名到RSA签名甚至加密。今天,我们' LL覆盖椭圆曲线和RSA-2048签名。

加密是一个很大的空间,但我们'重新关注不对称,或公钥加密。这些非对称加密计划工作的方式是他们拥有私钥和公钥。您使用私钥拍摄一些数据并创建它的签名,使用公钥来验证该键。验证基本上是一个真实性的检查,"这是由私钥签名的吗?"

作为姓名意味着,私钥是我们的秘密(即它从未分享过),公钥Ispublic。在我们的案例中,我们将在我们的申请代码中嵌入公钥。

在今天结束时,我们的加密许可证键将最终看起来像这样:

我们生成的许可证密钥的长度可能不同,具体取决于我们使用的加密方案,但格式将保持相同:某些编码数据,分隔符"。&#34 ;,和一个加密签名数据。 (这是对加密密钥的API使用的格式或多或少相同的格式。)

我们aren'今天将深入潜入椭圆形曲线加密(ECC),但这条链接应该遏制好奇。在ECC类别中,有一个无数的算法。今天,我们' ll探索ED25519,使用椭圆形曲线组进行了现代化的Schnorrignature系统。

ED25519提供了128位的安全级别,与AES-128,NIST P-256和RSA-3072相同的安全级别。 (意思是,是的,它' s很好。)

现在,而不是写下我们自己的加密,我们将使用节点' s标准加密模块,它支持节点12,支持ED25519。

const crypto =要求(' crypto')const {privatekey,puliethey} = crypto.generykeypaiveync(' ed25519')const签名key = privatekey.export({type:' pkcs8&# 39;格式:' der'})。ToString('十六进制和#39;)const verifykey = publickey.export({type:' spki'格式:&# 39; der'})。ToString('十六进制')console.log({签名键,reviewerkey})// => {// signingKey:' 302e020100300506032b657004220420a9466527e2b4dd30f202742abe38e8d75c9756a4f3d22daf1e37a317c22e2197' // verifyKey:' 302a300506032b657003210092f97e92cf06959a8b469d9da95609a4419fa2cc4f03a7009cd3a7c6bc1423e9' //}

在生成我们的钥匙后,我们'重新想要在Safeplace中保留这些编码的键。我们' ll使用keygen的私有签名密钥,我们' ll使用公共验证密钥来验证我们应用程序中许可证密钥的真实性。

//一些数据我们'重新嵌入许可证键盘数据=' [电子邮件保护]' //生成Dataconst Signature = Crypto.Sign的签名(null,buffer.from(数据),privateKey)//使用我们的签名keyconst encodedsignsigns = signature.tostring来对签名和数据集进行编码(' base64')const encodeddata = buffer.from(数据).tostring(' base64')//组合编码的数据和签名来创建许可证keyconst licencekey =`$ {EncodedData }。$ {EncodedSignature}`console.log({liceelskey})// => {LicenseKey:' dxnlckbjdxn0b21lci5legftcgxl.kanuxahc8b7rdngbfbposusmfkm7msqc0tnkeued4b5w15xf6zxmov3ayf54zawfmhznsny7m9blloinknvldw ==' }

什么'很大的关于这个许可证密钥格式,我们可以嵌入我们需要的任何数据集。现在,我们'重新嵌入客户'我们的电子邮件,但我们也可以覆盖其他信息,例如订单ID,键到期日期,权利和更多。 (它甚至可能是JSON对象,其实际上是我们API的默认值。)

一个缺点是您嵌入的数据越多,许可证键将变得更大。但在现实世界中,这不是一个问题,因为大多数用户的大多数用户都将粘贴他们的许可证钥匙,而不是 用手键入它们。 //通过delimiterConst [EncodedData,EncodedSignature] = LicenseKey.split('')//解码嵌入式数据及其signatureconst签名= ......