HarmonyOS 鸿蒙Next安全地在前后端之间传输数据

发布于 1周前 作者 sinazl 来自 鸿蒙OS

HarmonyOS 鸿蒙Next安全地在前后端之间传输数据

核心知识

在讲 JSEncrypt 之前,咱们回到“安全传输”这一主题。这一主题的关键技术在于加解密,说起加解密,那就是三大类算法:HASH(摘要)算法、对称加密算法和非对称加密算法。基本的安全传输过程可以用一张图来 展示:

不过这只是最基本的安全传输理论,实际上,证书(公钥)分发等方面仍然存在安全隐患,所以才会有CA、才会有受信根证书……不过这里不作延展,只给个结论:在 Web 前后端传输这个问题上,HTTPS 就是最佳实践,是首选 Web 传输解决方案,只有在不能使用 HTTPS 的情况,才退而求其次,用自己的实现来提高一点安全门槛。

JSEncrypt


JSEncrypt 一个月前刚有新版本,还算活跃。不过在使用方式上跟 RSA.js 不同,它不需要指定 RSA 的参数,而是直接导入一个 PEM 格式的密钥(证书)。关于证书格式呢,就不在这里科普了,总之 PEM 是一种文本格式,Base64 编码。

既然 JSEnrypt 需要导入密钥,这里主要是需要导入公钥。我们来看看 C# 里 RSACryptoServiceProvider 能导出些什么,搜了一下 Export… 方法,导出公约相关的主要就这两个:

因为原始需求是用 .NET,所以先研究 .NET 跟 JSEncrypt 的配合,后面再补充 NodeJS 和 Java 的。
  • ExportRSAPublicKey(),以 PKCS#1 RSAPublicKey 格式导出当前密钥的公钥部分。

  • ExportSubjectPublicKeyInfo(),以 X.509 SubjectPublicKeyInfo 格式导出当前密钥的公钥部分。

还有两个 Try… 前缀的方法作用相似,可以忽略。这两个方法的区别就在于导出的格式不同,一个是 PKCS#1 (Public-Key Cryptography Standards),一个是 SPKI (Subject Public Key Info)。

JSEncrypt 能导入哪种格式呢?文档里没明确说明,不妨试试。

C# 产生密钥并导出

C# 中产生 RSA 密钥对比较简单,使用 RSACryptoServiceProvider 就行,比如产生一对 1024 位的 RSA 密钥,并以 XML 格式导出:

// C# Code


private RSACryptoServiceProvider GenerateRsaKeys(int keySize = 1024)

{

    var rsa = new RSACryptoServiceProvider(keySize);

    var xmlPrivateKey = rsa.ToXmlString(true);

    // 如果需要单独的公钥部分,将传入 ToXmlString() 改为 false 就好

    // var xmlPublicKey = rsa.ToXmlString(false);


    File.WriteAllText(“RSA_KEY”, xmlPrivateKey);

    return rsa;

}


为了能在进程每次重启都使用相同的密钥,上面的示例将产生的 xmlPrivateKey 保存到文件中,重启进程时可以尝试从文件加载导入。注意,由于私钥包含公钥,所以只需要保存 xmlPrivateKey 就够了。那么加载的过程:


// C# Code


private RSACryptoServiceProvider LoadRsaKeys()

{

    if (!File.Exists(“RSA_KEY”)) { return null; }

    var xmlPrivateKey = File.ReadAllText(“RSA_KEY”);


    var rsa = new RSACryptoServiceProvider();

    rsa.FromXmlString(xmlPrivateKey);

    return rsa;

}


先尝试导入,不成再新生成的过程就一句话:


// C# Code


var rsa = LoadRsaKeys() ?? GenerateRsaKeys();



导出 XML Key 是为了持久化。JSEncrypt 需要的是 PEM 格式的证书,也就是 Base64 编码的证书。ExportRSAPublicKey 和 ExportSubjectPublicKeyInfo 这两个方法的返回类型都是 byte[],所以需要对它们进行 Base64 编码。这里使用 Viyi.Util 提供的 Base64Encode() 扩展方法来实现:


// C# Code


var pkcs1 = rsa.ExportRSAPublicKey().Base64Encode();

var spki = rsa.ExportSubjectPublicKeyInfo().Base64Encode();


严格的说,PEM 格式还应该加上 -----BEGIN PUBLIC KEY----- 和 -----END PUBLIC KEY----- 这样的标头标尾,Base64 编码也应该按每行 64 个字符进行折行处理。不过实测 JSEncrypt 导入时不会要求这么严格,省了不少事。

剩下的就是将 pkcs1 和 spki 传递给前端了。Web 应用直接通过 API 返回一个 JSON,或者 TEXT 都行,根据接口规范来决定。当然也可以通过拷贝/粘贴的方式来传递。这里既然是在做实验,那就用 Console.WriteLine 输出到控制台,通过剪贴板来传递好了。

我这里 PKCS#1 导出的是长度为 188 个字符的 Base64:

MIGJAoGB…tAgMBAAE=


SPKI 导出的是长度为 216 个字符的 Base64:


MIGfMA0GC…QIDAQAB


JSEncrypt 导入公钥并加密

JSEncrypt 提供了 setPublicKey() 和 setPrivateKey() 来导入密钥。不过文档中提到它们其实都是 setKey() 的别名,这点需要注意一下。为了避免语义不清,我建议直接使用 setKey()

You can use also setPrivateKey and setPublicKey, they are both alias to setKey

from: http://travistidwell.com/jsen

那么导入公钥并试验加密的过程大概会是这样:

// JavaScript Code


const pkcs1 = “MIGJAoGB…tAgMBAAE=”;   // 注意,这里的 KEY 值仅作示意,并不完整

const spki = “MIGfMA0GC…QIDAQAB”;     // 注意,这里的 KEY 值仅作示意,并不完整


[pkcs1, spki].forEach((pKey, i) => {

    const jse = new JSEncrypt();

    jse.setKey(pKey);

    const eCodes = jse.encrypt(“Hello World”);

    console.log([${i} Result]: ${eCodes});

});


运行后得到输出(密文也是省略了中间很长一串的 ):


[0 Result]: false

[1 Result]: ZkhFRnigoHt…wXQX4=


看这结果,没啥悬念了,JSEncrypt 只认 SPKI 格式

不过还得去 C# 中验证这个密文是可以解出来的。

C# 验证可以解密 JSEncrypt 生成的密文

上面生成的那一段 ZkhFRnigoHt…wXQX4= 拷贝到 C# 代码中,用来验证解密。C# 使用 RSACryptoServiceProvider.Decrypt() 实例方法来解密,这个方法的第 1 个参数是密文,类型 byte[],是以二进制数据的形式提供的。

第二个参数可以是 boolean 类型,true 表示使用 OAEP 填充方式,false 表示使用 PKCS#1 v1.5;这个参数也可以是 RSAEncryptionPadding 对象,直接从预定义的几个静态对象中选择一个就好。这些在文档中都说得很清楚。因为一般都是使用的 PKCS 填充方式,所以这次赌一把,直接上:


// C# Code


var eCodes = “ZkhFRnigoHt…wXQX4=”;    // 示例代码这里省略了中间大部分内容

var rsa = LoadRsaKeys();   // rsa 肯定是使用之前生成的密钥对,要不然没法解密

byte[] data = rsa.Decrypt(eCodes.Base64Decode(), false);

Console.WriteLine(data.GetString());    // GetString 也是 Viyi.Util 中定义的扩展方法,默认用 UTF8 编码


结果正如预期:


Hello World


技术总结

现在,通过实验,Web 前端使用 JSEncrypt 和 .NET 后端之间已经实现了 RSA 加/解密来完成安全的数据传输。其做法总结如下:

  1. 后端产生 RSA 密钥对,保存备用。保存方式可根据实际情况选择:内存、文件、数据库、缓存服务等

  2. 后端以 SPKI 格式导出公钥(别忘了 Base64 编码),通过某种业务接口形式传递给前端,或由前端主动请求获得(比如调用特定 API)

  3. 前端使用 JSEncrypt,通过 setKey() 导入公钥,使用 encrypt() 加密字符串。加密前字符串会按 UTF8 编码成二进制数据。

  4. 后端获得前端加密后的数据(Base64 编码)后,解密成二进制数据,并使用 UTF8 解码成文本。

特别需要注意的一点是:不管以何种方式(XML、PEM 等)将公钥传送给前端的时候,都切记不要把私钥给出去了。这尤其容易发生在使用 .ToXmlString(true) 之后再直接把结果送给前端。不要问我为什么会有这么个提醒,要问就是因为……我见过!

关门放 Node

还没完呢,前面说过要补充 NodeJS 后端的情况。NodeJS 关于加/解密的 SDK 都在 crypto 模块中,

  • 使用 generateKeyPair() 或 generateKeyPairSync() 来产生密钥对

  • 使用 privateDecrypt() 来解密数据

generateKeyPair() 是异步操作。现在 Node 中异步函数很常见,尤其是写 Web 服务端的时候,到处都是异步。不喜欢回调方式的话,可以使用 util 模块中的 promisify() 把它转换一下。

// JavaScript Code, in Node environtment


import { promisify } from “util”;

import crypto from “crypto”;


const asyncGenerateKeyPair = promisify(crypto.generateKeyPair);


(async () => {

    const { publicKey, privateKey } = await asyncGenerateKeyPair(

        “rsa”,

        {

            modulusLength: 1024,

            publicKeyEncoding: {

                type: “spki”,

                format: “pem”,

            },

            privateKeyEncoding: {

                type: “pkcs1”,

                format: “pem”

            }

        }

    );


    console.log(publicKey)

    console.log(privateKey);

})();


generateKeyPair 第 1 个参数是算法,很明显。第 2 个参数是选项,强度 1024 也很明显。只有 publicKeyEncoding 和 privateKeyEncoding 需要稍微解释一下 —— 其实文档也说得很明白:参考 keyObject.export()

对于公钥,type 可选 “pkcs1” 或者 “spki”,之前已经试过,JSEncrypt 只认 “spki”,所以没得选。

对于私钥,RSA 只能选 “pkcs1”,所以还是没得选。

不过 NodeJS 的 PEM 输出要规范得多,看(同样省略了中间部分):

-----BEGIN PUBLIC KEY-----

MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYur0zYBtqOqs98l4rh1J2olBb

… … …

8I8y4j9dZw05HD3u7QIDAQAB

-----END PUBLIC KEY-----

-----BEGIN RSA PRIVATE KEY-----

MIICXAIBAAKBgQCYur0zYBtqOqs98l4rh1J2olBbYpm5n6aNonWJ6y59smqipfj5

… … …

UJKGwVN8328z40R5w0iXqtYNvEhRtYGl0pTBP1FjJKg=

-----END RSA PRIVATE KEY-----


不管是否含标头/标尾,也不管是不是有折行,JSEncrypt 都认,所以倒不用太在意这些细节。总之 JSEncrypt 拿到公钥之后还是跟之前一样,做同样的事情,逻辑代码一个字都不用改。


然后回到 NodeJS 解密:


// JavaScript Code, in Node environtment


import crypto from “crypto”;


const eCodes = “ZkhFRnigoHt…wXQX4=”;    // 作为示例,偷个懒就用之前的那一段了

const buffer = crypto.privateDecrypt(

    {

        key: privateKey,

        padding: crypto.constants.RSA_PKCS1_PADDING

    },

    Buffer.from(eCodes, “base64”)

);


console.log(buffer.toString());


privateDecrypt() 第 1 个参数给私钥,可以是之前导出的私钥 PEM,也可以是没导出的 KeyObject 对象。需要注意的是必须要指定填充方式是 RSA_PKCS1_PADDING,因为文档说默认使用 RSA_PKCS1_OAEP_PADDING

还有一点需要注意的是别忘了 Buffer.from(…, “base64”)

解密的结果是保存在 Buffer 中的,直接 toString() 转成字符串就好,显示指定 UTF-8,用 toString(“utf-8”) 当然也是可以的。

等等,还有 Java 呢

Java 也大同小异,不过说实在,代码量要大不少。为了干这些事情,大概需要导入这么些类:


// Java Code


import java.nio.charset.StandardCharsets;

import java.security.KeyFactory;

import java.security.KeyPair;

import java.security.KeyPairGenerator;

import java.security.spec.PKCS8EncodedKeySpec;

import java.util.Base64;

import java.util.Base64.Decoder;

import java.util.Base64.Encoder;

import javax.crypto.Cipher;


然后是产生密钥对


// Java Code


KeyPairGenerator gen = KeyPairGenerator.getInstance(“RSA”);

gen.initialize(1024);

KeyPair pair = gen.generateKeyPair();


Encoder base64Encoder = Base64.getEncoder();

String publicKey = base64Encoder.encodeToString(pair.getPublic().getEncoded());

String privateKey = base64Encoder.encodeToString(pair.getPrivate().getEncoded());


// 这里输出 PKCS#8,所以解密时需要用 PKCS8EncodedKeySpec

System.out.println(pair.getPrivate().getFormat());


产生的 publicKey 和 privateKey 都是纯纯的 Base64,没有其他内容(没有标头/标尾等)。

然后是解密过程……


// Java Code


String eCode = “k7M0hD…qvdk=”;  // 再次声明,这是仅为演示写的阉割版数据


Decoder base64Decoder = Base64.getDecoder();

PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(base64Decoder.decode(privateKey));

KeyFactory keyFactory = KeyFactory.getInstance(“RSA”);


Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());

cipher.init(Cipher.DECRYPT_MODE, keyFactory.generatePrivate(keySpec));

byte[] data = cipher.doFinal(base64Decoder.decode(eCode));


System.out.println(new String(data, StandardCharsets.UTF_8));




作者:边城                  

原链接:https://segmentfault.com/a/1190000039827138




关于HarmonyOS 鸿蒙Next安全地在前后端之间传输数据的问题,您也可以访问:https://www.itying.com/category-93-b0.html 联系官网客服。

4 回复

真的不错,写的很细致   

必须要关注,哈哈哈   

必须顶起来,好内容   

鸿蒙这边怎么实现啊,有没有大佬贴一下
回到顶部