Node.js RSA加密解密与Java实现的对比

近期在对接一个后端服务时,要求请求过程中需要使用RSA加密。请求体需要使用公钥加密,返回体需要使用私钥解密。

接口方提供了Java代码作为参考, 本以为很简单,只是做一下代码翻译。开干的过程才发现没有想象中的容易。 时间主要花在了对加密这一块不熟悉,然后很难判断证书使用不了的原因。整体网上资料也很少, 最后只能通过同时debug Java代码以及Nodejs代码,逐步排查变量。辛酸。。

省流总结:

  1. Java代码在读取公钥私钥的过程中, 不需要添加行首行尾, 类似 -----BEGIN PRIVATE KEY----- 以及 -----END PRIVATE KEY-----, 而Node.js 需要.
  2. 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;
  }
}