Node.js RSA加密解密与Java实现的对比
近期在对接一个后端服务时,要求请求过程中需要使用RSA加密。请求体需要使用公钥加密,返回体需要使用私钥解密。
接口方提供了Java代码作为参考, 本以为很简单,只是做一下代码翻译。开干的过程才发现没有想象中的容易。 时间主要花在了对加密这一块不熟悉,然后很难判断证书使用不了的原因。整体网上资料也很少, 最后只能通过同时debug Java代码以及Nodejs代码,逐步排查变量。辛酸。。
省流总结:
- Java代码在读取公钥私钥的过程中, 不需要添加行首行尾, 类似
-----BEGIN PRIVATE KEY-----
以及-----END PRIVATE KEY-----
, 而Node.js 需要. - Java默认使用的padding 与nodejs不同, nodejs在加载秘钥时需要指定padding. 具体padding 可以从
crypto.constants.RSA_PKCS1_PADDING
获取,使用下面类似代码。const encryptedBlock = crypto.publicEncrypt( { key: publicKey, padding: crypto.constants.RSA_PKCS1_PADDING, }, dataBuffer.slice(offset, inputLen + offset), );
接下来是完整的请求保证代码。
import axios from 'axios';
import crypto from 'crypto';
import md5 from 'md5';
interface RequestBody {
account: string;
data: string;
ts: number;
}
interface SignedRequestBody extends RequestBody {
sign: string;
}
interface SignedResponseBody {
account: string;
encrypt: boolean;
data: string;
ts: number;
sign: string;
}
const RESERVE_BYTES = 11;
export class RequestExecutor {
public host: string;
public publicKey = '';
public privateKey = '';
public account = '';
public decryptBlock = 256;
public encryptBlock = this.decryptBlock - RESERVE_BYTES;
public constructor(
host: string,
account: string,
publickKey: string,
privateKey: string,
) {
this.host = host;
this.account = account;
this.publicKey = publickKey;
this.privateKey = privateKey;
}
public async request<T = any>(api: string, data: Record<string, any> = {}) {
const res = await axios.request<SignedResponseBody>({
url: `${this.host}${api}`,
method: 'POST',
data: this.getRequestBody(data),
});
const resBody = this.decryptBody<T>(res.data.data);
return resBody;
}
private getRequestBody(body: Record<string, string>) {
const requestBody: RequestBody = {
ts: Date.now(),
account: this.account,
data: this.encryptBody(body),
};
const sign = this.getSignature(requestBody);
return {
...requestBody,
sign,
} as SignedRequestBody;
}
private getSignature(body: RequestBody) {
let plainText = '';
Object.keys(body)
.sort()
.forEach(
key =>
(plainText = `${plainText}${key}${body[key as keyof RequestBody]}`),
);
return md5(plainText);
}
private encryptBody(body: Record<string, string>) {
const publicKey = `-----BEGIN PUBLIC KEY-----\n${this.publicKey}\n-----END PUBLIC KEY-----`;
if (body !== undefined) {
const dataBuffer = Buffer.from(JSON.stringify(body), 'utf-8');
const dataLen = dataBuffer.length;
let encryptedBuffer = Buffer.alloc(0);
for (let offset = 0; offset < dataLen; offset += this.encryptBlock) {
// block大小: encryptBlock 或 剩余字节数
let inputLen = dataLen - offset;
if (inputLen > this.encryptBlock) {
inputLen = this.encryptBlock;
}
const encryptedBlock = crypto.publicEncrypt(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_PADDING,
},
dataBuffer.slice(offset, inputLen + offset),
);
encryptedBuffer = Buffer.concat(
[encryptedBuffer, encryptedBlock],
encryptedBuffer.length + encryptedBlock.length,
);
}
return encryptedBuffer.toString('base64');
}
throw new Error('Set request data before request');
}
private decryptBody<T>(encryptedStr: string) {
const privateKey = `\n-----BEGIN PRIVATE KEY-----\n${this.privateKey}\n-----END PRIVATE KEY-----\n`;
const encryptedBuffer = Buffer.from(encryptedStr, 'base64');
const dataLen = encryptedBuffer.length;
let decryptedBuffer = Buffer.alloc(0);
for (let offset = 0; offset < dataLen; offset += this.decryptBlock) {
// block大小: encryptBlock 或 剩余字节数
let inputLen = dataLen - offset;
if (inputLen > this.decryptBlock) {
inputLen = this.decryptBlock;
}
const dencryptedBlock = crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PADDING,
},
encryptedBuffer.slice(offset, inputLen + offset),
);
decryptedBuffer = Buffer.concat(
[decryptedBuffer, dencryptedBlock],
decryptedBuffer.length + dencryptedBlock.length,
);
}
return JSON.parse(decryptedBuffer.toString('utf8')) as T;
}
}