Nodejs开发进阶E-Crypto之非对称加密

此篇是《Nodejs开发进阶D-Crypto》的延续部分,因为在前文中已经简单提到,非对称加密相关的内容和操作比较多,这里我们专门单独使用一个章节来讨论,主要内容就是crypto模块中非对称加密相关的内容。

在单独阅览本文之前,建议先阅读了解前面的内容,在:

《Nodejs开发进阶D-Crypto》:juejin.cn/post/731679...

这里需要先说明,笔者并非专业的密码学研究专家,对非对称加密的理解和掌握还在一个逐步明晰的过程当中,也没有时间和精力进行过于深入的研究,主要是从开发者和应用的角度,来理解这些问题。很多概念和看法可能有不完善成熟,也有可能有缺失或者错误的地方,仅代表笔者的理解供读者参考。如果读者看到和发现这些不足,希望不吝指教。

加密体系

对于非对称加密的密码学技术和实践,其实也是分为两大体系的。就是RSA和ECC。

RSA是比较经典的非对称加密算法,其理论基础其实很简单,就是大质数乘积的分解困难性;而ECC(Elliptic Curve Cryptography,椭圆曲线密码学)的理论基础是是椭圆曲线数学(图),相对更复杂一点,其理论和实践也比较新颖。ECC的主要优势是对于同等的安全性,相对而言密钥的长度比较小,相关计算如密钥生成、加解密、签名验证等操作性能比较好,所以在移动互联网时代的应用日益广泛。

另外,通过选择不同类型的椭圆曲线,ECC算法的灵活性也较高,而RSA的安全性扩展主要通过增加密钥长度达成,代价也比较高,这些都可以认为它可以提供比RSA更好的安全性。

这两种加密体系,对于开发和应用的影响,主要就是相关操作的配置和参数略有不同,但应用方式基本上是一样的,所以都可以归为非对称加密体系,这些需要开发者熟悉和理解。

密钥对象(KeyObject,KO)

为了方便管理和使用密钥,特别是非对称加密相关的密钥,包括各种生成、转换、加载等操作,nodejs crypto模块提供了KeyObject类。比如crypto中,很多使用密钥的方法,除了密钥的原始形式(buffer或者字符串编码)之外,还可以接受KO对象,从而提高更丰富的特性和控制能力。

从本质上而言,所有密钥的核心内容,就是一个二进制对象(buffer或者字节数组,base64字符串)。但需要一些外部的参数和设置来描述这个密钥,方便进行使用、交换、转换或者存储等。比如密钥的类型、编码方式、格式,都是需要在开发过程中正确理解和应用的。下面列出的KO相关的常用方法和属性能够帮助我们了解其主要操作方式和特性:

  • type: 密钥类型,表明此密钥是对称密钥(secret),公钥(public)或者私钥(private)
  • symmetricKeySize: 对称密钥长度,非对称密钥此属性不可用
  • asymmetricKeyType/asymmetricKeyDetails: 非对称密钥的类型和参数细节
  • equals: 判断两个密钥是否相同,包括类型,参数和内容
  • export: 依据相关的设置和格式,导出密钥
  • createSecretKey: 基于一个buffer或者编码字符串,创建一个普通的安全密钥对象
  • createPrivateKey/creatPublicKey: 从一个buffer或者编码字符串,以及相关的配置信息,创建一个非对称密钥对象(私钥或者公钥)
  • from: 这个方法比较特别,是将CryptoKey(WebCrypto标准)转换为KO

一个密钥的生命周期,可能会包括密钥的创建-导出-传输-加载-应用等环节。

  • 创建

我们会在信息加解密操作,和其他密钥学操作如HMAC中频繁的使用各种密钥。对于对称加密类型的临时密钥,我们通常使用随机信息的产生机制来生成。但可能一个更好的实践方式是使用generateSecretKey方法来创建一个需要的密钥。非对称加密算法的临时密钥,则需要通过特定的密钥对生成方法来创建(详见后续章节)。

  • 导出

为了使密钥持久化,或者用于传输,KO显然是不能直接使用的。这时我们可以使用export方法将其导出。导出时有一些参数设置,可以定义导出的格式和形式,通常都是一些标准规范的,也利用其他的系统接收和还原。

  • 传输

密钥导出的结果通常就是一个带格式的字符串,可以直接用于文件存储和网络传输。但一般的安全规范认为,除了公钥之外,其他类型的密钥,特别是私钥,不应该使用网络进行传输。

  • 加载

加载就是将密钥格式字符串,或者文件,加载并转换成为密钥对象。在crypto中可以使用createSecretKey, createPrivateKey、createPublicKey等方法来操作。

  • 应用

crypto模块中,很多密码学操作方法,可以使用KO对象,而不是直接使用密钥buffer,可以简化一些附加的参数设置环节。

密钥对生成

非对称加密的基本工作方式,就是使用一套两个密钥,在不同的应用场景中组合使用和操作。所以在实际进行任何非对称的加密操作之前,我们都需要先准备好相关的密钥。这个密钥生成的操作,所输出的密钥总是成对的出现。公钥是可以公开并分享给其他人的,私钥则需要避免其他人能够来访问和读取,一定要保证其私有和机密。

nodejs通常使用generateKeyPair方法和一些设置参数来生成一个密钥对,并可以选择按照一些标准格式进行输出(原始密钥都是buffer),如下面的参考代码:

js 复制代码
const crypto = require("crypto");

const 
CONFIG = {
    ALGO_EC : "ec",
    CURVE     : 'secp256k1',
    KEYPASS   : "topsecret"
},
publicKeyEncoding = {
    type: 'spki',
    format: 'pem',
},
privateKeyEncoding = {
    type: 'pkcs8',
    format: 'pem',
    cipher: "AES-256-CBC" ,
    passphrase: CONFIG.KEYPASS,
};

const { 
    publicKey, 
    privateKey 
} = crypto.generateKeyPairSync(CONFIG.ALGO_EC, {
    namedCurve: CONFIG.CURVE,
    publicKeyEncoding,privateKeyEncoding
});

console.log(publicKey, privateKey);

// 结果形式如下
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEBieT83YaD4hg7F/+0MY1zn481S3YzBFX
qlQXMD6wNuzRn655/OVQdqOrKUeiKAmIczQ6dDNz5JnjuX4urGWDBA==
-----END PUBLIC KEY-----

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAjC91xkSsvzLwICCAAw
DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEK7XhLhuN7tCkxIVMFTpMVoEgZD+
qNU7hcSt+ANgxfGK+MhuyvvQM4p5h1qCI0BGGrdLcDcCIBAEbSNxpcfHjVUkkhLT
kDycHXYnwm7EX+0lVIW7JLF2Z7VykSnuI/N20JpYWr/oAuc0A8LZHmFp5xYuWYPm
Njkg70IO5CbcNv7HgetUmhG+bv2s4K/nLGYMX9b2B0pyyH6fPj74ljHOric17kQ=
-----END ENCRYPTED PRIVATE KEY-----

这段代码我们可以看到几个需要注意的地方:

  • 我们选择的非对称加密算法是ECC椭圆曲线,它相对而言比RSA要先进和高效
  • 所选择的曲线类型是secp256k1,这个设置也是使用比较广泛的设置(比特币技术使用此参数)
  • 密钥对生成的结果,默认情况下是一对buffer,可以选择输出编码后的形式(格式字符串),详见配置参数
  • 公钥和私钥所使用的编码格式和规范略有差异
  • 私钥输出可选使用密码加密来提高安全性
  • 常用的pem格式,就是一个字符串,有特定的开始和结束行(BEGIN KEY / END KEY ),中间是base64编码的密钥内容

前面对于密钥对象的生命周期管理中,我们已经提到,我们生成密钥后,可以将其导出,并保存在文件或者配置信息中,然后在后续的应用中,按照需要(如启动时)加载,然后使用,这样可以保证一个加密机制的相对稳定性,而不是每次都动态生成和管理。

这里要特别注意,对于私钥,使用密码来保护私钥可以提供更高的安全性,但需要增加两个扩展属性的管理:

  • cipher: 加密使用的算法,如aes-256-cbc
  • passphrase: 加密使用的密码短语

最后,我们会在很多地方都看到不同的密钥对,包括非对称加解密、密钥协商、签名和验证,理论上这些密钥对的生成和关系都是不同的,最好不要混用。这会带来一些开发和管理上的麻烦,但这可能确实是为了安全所付出的一些代价吧。

公钥加密-私钥解密

非对称加密,最常见的应用场景,就是公钥加密,私钥解密。例如,如果Chalie要向Steve传送信息,他需要先获得Steve的公钥,然后使用此公钥对原文进行加密后传输给Steve,随后Steve使用自己的私钥,对密文进行解密得到原文。对于第三方,可能会截获密文,即使也能获取Steve的公钥,但因为没有私钥,也是无法正确解密的。

crypto提供了相关的方法可以完成这一操作,但这些方法只支持RSA,所以实例代码中,我们使用RSA类型和方法,相关代码如下:

rsa.js 复制代码
const 
crypto = require("crypto"),
{ publicKey, privateKey } = crypto.generateKeyPairSync("rsa",{ modulusLength: 2048 });

console.log("Public Key:", publicKey.export({ type: "spki", format: "pem" }));
console.log("Private Key:", privateKey.export({type: "pkcs8", format: "pem"}));

const otext = Buffer.from("china中国");

let e = crypto.publicEncrypt(publicKey,otext).toString("base64");

console.log("Public Encrypted:", e);

let d = crypto.privateDecrypt(privateKey,Buffer.from(e,"base64")).toString();

console.log("Private Decrypted:", d);


e = crypto.privateEncrypt(privateKey, otext).toString("base64");
console.log("Private Encrypted:", e);

d = crypto.publicDecrypt(publicKey, Buffer.from(e,"base64")).toString();
console.log("Public Decrypted:", d);

私钥加密-公钥解密

同样,只有RSA支持此操作。操作方式基本上和公钥加密私钥解密相同,前面的代码已经有所展示。

这里主要是需要讨论一下这种操作方式的应用场景。信息发布者,使用私钥对信息进行加密;接受信息者,都可以使用发布者的公钥对信息进行正确的解密,这个过程,可以保证这个信息确实是由发布者发布的(假设这个公钥不是伪造的化)。这不就是一种信息的完整性和不可抵赖性验证的方式吗?

笔者觉得,这本质上就是签名和验证操作的技术基础。因为,我们完全可以想象一个基于私钥加密的的签名应用过程,来满足信息传输完整性和不可抵赖性的安全需求。过程如下(先不考虑加密环节):

  • 对原始信息进行摘要操作,得到信息的摘要
  • 签名方,对摘要信息,使用私钥进行加密,加密的结果作为签名发布
  • 验证方,获得原文后,用同样的方式计算信息摘要
  • 验证方,对签名,使用发布方的公钥进行解密,获得摘要信息
  • 验证方,比较解密后的摘要和计算出的摘要,确认原文的完整性,以及此信息签名确实来自签名方

密钥协商(Diffie-Hellman Key Exchange,DHKE)

使用对称加密方式,对信息进行加密和解密,这个过程的信息安全性本身是没有什么问题的。但是,这里有一个结构性的问题,就是加密和解密在逻辑上总是两个独立的实体,所以如果安全的共享相同的密钥,就是加密方,如何将密钥传输给解密方,就成为一个无解的问题。

这是一个图灵奖级别的问题。在1976年,来自斯坦福大学的Whitfield Diffie和Martin Hellman(图),在他们的论文《New Directions in Cryptography》(密码学的新方向)中,明确的提出了一种基于公钥技术的安全密钥交换方式,被认为是密钥协商算法的开端。这个算法自然也以DH为名。后来,由于在密码学上天才而突出的贡献,他们获得了2015年的图灵奖。

DH的算法的基本模型其实也不难理解,如下图所示(来自Wiki):

当然,DH算法的背后的理论基础是离散数学,我们这里不加以深究。我们需要理解的是,除了公钥之外,在加密的双方,没有真实的传递任何机密信息(公钥本身是有私钥计算并且公布出来的,没有指向性),最后的密钥,是通过数学和逻辑规则计算得到的,这样就避免了可能的机密信息传输泄露的风险。

和非对称加密一样,DH也有不同的技术实现方案。在crypto中,主要有DH和ECDH两种。和RSA/ECC的情况类似,ECDH也是基于椭圆曲线理论,一般认为它能够提供比经典DH算法更高的安全性,更小的密钥长度、更小的复杂程度和更好的性能,如果没有其他特别的原因,开发者应该优先选择这一技术方案。

下面分别是crypto模块使用DH和ECDH方法实现密钥协商的示例代码:

dh.js 复制代码
const 
crypto = require("crypto"),
ECDH = {
    Curve: "secp256k1"
};

let alice, bob, alicePubKey, bobPubKey, aliceSec, bobSec;

console.log("------------- DH -----------------");
// Generate Alice's keys...
alice = crypto.createDiffieHellman(2048);
alicePubKey = alice.generateKeys();
  
// Generate Bob's keys...
bob = crypto.createDiffieHellman(alice.getPrime(), alice.getGenerator());
bobPubKey = bob.generateKeys();
  
// Exchange and generate the secret...
aliceSec = alice.computeSecret(bobPubKey);
bobSec = bob.computeSecret(alicePubKey);
  
console.log("Key1:",aliceSec);
console.log("Key2:",bobSec);

console.log("------------- ECDH -----------------");

// Generate Alice's keys...
alice     = crypto.createECDH(ECDH.Curve);
alicePubKey = alice.generateKeys();

// Generate Bob's keys...
bob = crypto.createECDH(ECDH.Curve);
bobPubKey = bob.generateKeys();
  
  // Exchange and generate the secret...
aliceSec = alice.computeSecret(bobPubKey);
bobSec   = bob.computeSecret(alicePubKey);

console.log("Key1:",aliceSec);
console.log("Key2:",bobSec);

要点和简单分析如下:

  • crypto提供了DiffieHellman类和ECDH类,它们的使用方式类似
  • create方法可以创建DH类实例,其实就是私钥,注意相关的参数和设置
  • DH实例提供了generateKeys,可以获得该实例对应的公钥
  • 以外部公钥为参数,使用实例的createSercet方法,可以计算协商后的密钥
  • DH的速度(十数秒)明显慢于ECDH(立即),在本例中可能需要数秒
  • 生成的密钥长度,ECDH也明显比DH要短

协商生成密钥后,就可以作为对称加密的密钥进行对称加密计算了,其实这就是HTTPS工作的原理。AES等对称加密的性能很高,但密钥的传输有安全隐患;而非对称加密可以使用协商方式生成密钥提供给对称加密使用,也规避了非对称加密性能不高的缺点;两者组合使用,扬长避短,获得了性能和安全的良好的平衡。

这里本来笔者有一个疑问,就是也可以利用非对称加密来交换密钥。就是在传输方,先随机生成一个临时用于对称加密的密钥,然后使用接受方的公钥进行加密,将加密后的密钥传输给接受方,接受方使用自己的私钥进行解密,同样完成了密钥协商的过程。笔者没有对此过程的安全性进行评估,但个人觉得这个方案确实不如DH,理由如下:

  • 不够优雅,理由就是随后的几点
  • 不够对称,需要由一方先生成
  • 需要额外传输信息,可能有安全隐患
  • 优点是密钥是动态的,但DH也可以使用密钥衍生算法加强

签名和验证(Sign/Verify)

签名和验证,虽然是两个操作环节,但在密码学中的逻辑关联是非常紧密的,所以需要放在一起讨论。在信息安全和密码学范畴中,签名和验证解决的是信息的完整性和不可抵赖性的问题。

完成的过程分为签名和验证两个方面,通常也是信息的发送方和接收方。通过签名验证方法,接收方可以确认发送方发送的信息是完整的,并且确实是由发送方发送的,而发送方由于进行了签名的操作,不能否认这一点。在非对称加密算法的应用过程中,通常的操作过程是(先不考虑加解密的情况):

  • 发送方准备信息原文
  • 发送方使用自己的私钥,对原文进行签名计算,得到签名信息
  • 发送方将原文连同签名信息发送给接收方
  • 接收方接收到两个信息后,使用发送方的公钥,对原文和签名信息进行验证
  • 如果验证通过,则表明此原文是完整的,并且被发送方认可(私钥签名)
  • 如果验证不通过,则表明原文可能被破坏,或者签名信息错误,无法确认其来自发送方

crypto提供了Sign和Verify类,来进行签名和验证的操作。相关示例代码如下:

js 复制代码
const 
crypto = require("crypto"),
{ publicKey, privateKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'sect239k1',});

const otext = "china中国";

// sign phase
const sign = crypto.createSign('SHA256');
sign.write(otext);
sign.end();

const signature = sign.sign(privateKey, 'base64');
console.log("Sign", signature);

// verify phase
const verify = crypto.createVerify('SHA256');
verify.write("china中国");
verify.end();

console.log("Verify: ", verify.verify(publicKey, signature, 'base64'));

const verify2 = crypto.createVerify('SHA256');
verify2.write(otext+1);
verify2.end();
console.log("Verify2:", verify2.verify(publicKey, signature, 'base64'));

我们可以从这段代码中看到:

  • 本例中使用ECC密钥对
  • createSign和creatVerify创建的sign和verify实例
  • 创建参数使用的是一个摘要方法,它用于处理原文信息
  • 使用签名实例的write和end方法加载数据,然后此实例将计算内容摘要
  • 使用签名实例的sign方法对摘要进行签名,可以指定签名用的私钥,和输出格式
  • 签名结果可选输出格式,考虑到通常用于传输,一般是base64字符串
  • 验证实例同样需要先加载数据,并计算摘要
  • 验证使用验证实例的verify方法进行验证,参数是签名方的公钥、签名信息和编码方式
  • 验证的结果是简单的ture或者false

数字证书

使用公私钥加密,结合对称加密的方式,可以处理信息在传输过程中的安全保障问题,但其实没办法解决信息所属实体的问题,因为任何人都可以自己生成非对称密钥来处理信息。这不是一个信息技术的问题,而是一个社会工程的问题。为此,人们开发了CA(Certificate Authority,证书颁发机构)系统和公钥基础设施(Public Key Infrastructure),从而构成了现代社会大规模开放式信息系统和信息交换的基础安全框架。

这部分内容笔者不想展开讨论,只从技术和应用的角度来简单阐述一下这个体系的基本原理和工作方式。

  • 数字证书的技术基础就是非对称加密,签名和验证机制
  • 信息业务操作的实体,使用数字证书来标识和代表自己进行信息技术和安全方面的操作(提供网络服务、对信息进行加密等等)
  • 所谓数字证书,本质上就是一段数字信息,主要包括了实体信息、公钥、域名、用途、CA颁发机构的信息和签名等
  • 证书管理使用注册和审核机制,就是实体可以向CA提交证书申请,CA会对申请信息进行审核,确认后生成证书交付给实体使用
  • 任何信息操作的实体和应用程序,都可以用某种方式(如在线验证)对对方提供的数字证书进行验证,从而保障信息技术操作的可信性,这一过程可以实现程序化和自动化
  • 证书的颁发机构是层次化的,层次越高,可信度越强,所以操作系统或浏览器等基础软件会内置少数顶层的CA机构的证书(根证书)
  • 层次化的信任关系,也可以通过证书构成信任链,用于链式的认证操作,这样大大提高了证书应用的灵活性,并且可以有效分担信任责任,从而构成完整的信任和安全体系
  • 证书可以提供有效时限,到期后必须更新,从而提供时效安全性
  • CA可以提供证书吊销机制,使用方可以自动查询并确认失效的证书,提高安全性

图为百度证书的基本信息和层次结构,任何一个浏览器都会提供证书信息查看的功能。

可以看到,百度使用的根证书是GlobalSign,但并不是由它直接颁发,而是其下面的一个专门用于颁发证书的下属机构所颁发。

nodejs的crypto模块,通过X509Certificate类,提高了X509类型证书的相关操作功能,这也是现在数字证书技术体系的事实标准。但这个功能笔者在实践中并没有机会使用和操作,这里就不再展开。

最后,还要说明,证书解决的是实体可信和验证的问题,并非信息安全本身的问题。所以,使用过期,或者自签名的证书进行通信(典型的如访问网站),通信的过程和信息的完整还是安全的,但不能保障那个网站就是它所声称的,或者你想要访问的那个网站。这个概念和认知,作为系统开发者应该熟悉和了解。

小结

本文作为《Nodejs开发进阶D-Crypto》的第二个部分,讨论了关于非对称加密以及和crypto模块结合的相关问题。包括非对称加密的基本概念、密钥对象、密钥对生成和管理、公私钥加解密、密钥协商、签名和验证、数字证书基本概念等等,希望能够帮助读者对crypto模块中非对称加密技术和实现有一个基本的了解和认知。

相关推荐
架构文摘JGWZ3 分钟前
FastJson很快,有什么用?
后端·学习
BinaryBardC3 分钟前
Swift语言的网络编程
开发语言·后端·golang
邓熙榆12 分钟前
Haskell语言的正则表达式
开发语言·后端·golang
古蓬莱掌管玉米的神3 小时前
vue3语法watch与watchEffect
前端·javascript
专职3 小时前
spring boot中实现手动分页
java·spring boot·后端
拉一次撑死狗3 小时前
Vue基础(2)
前端·javascript·vue.js
Ciderw3 小时前
Go中的三种锁
开发语言·c++·后端·golang·互斥锁·
m0_748246354 小时前
SpringBoot返回文件让前端下载的几种方式
前端·spring boot·后端
m0_748230444 小时前
创建一个Spring Boot项目
java·spring boot·后端
卿着飞翔4 小时前
Java面试题2025-Mysql
java·spring boot·后端