鸿蒙开发RSA加解密实现方案

鸿蒙开发入门篇

Posted by Alee on March 28, 2024

此方案基于ArkTs API9 实现,解决了分段加解密,解密后中文乱码的问题

背景

随着鸿蒙Next即将发布,纯血鸿蒙将不再兼容Android APK应用的呼声也越来越多,且我司的Android应用因为上架了华为应用市场,最近也一直被华为官方的人员邀请加入鸿蒙开发大家庭。此时作为一个Android开发人员,不得不考虑开始适配鸿蒙系统了。趁着工作的间隙,看了一礼拜的华为鸿蒙开发指南,大概了解下语法就准备开始上手。这第一个难点就来了:我司的网络接口为了数据传输安全,所有请求参数都是组装完一个json串之后,整体用RSA进行加密一次,然后把加密后的字符串传给后台。由于加密内容太长,这就需要开始研究鸿蒙的RSA如何分段加解密了。经过各种探索和失败之后,于是就有了这篇博文。

Android项目已有的基于Java的RSA加解密方案

public static final String KEY_ALGORITHM = "RSA";//加密算法
private static final int MAX_ENCRYPT_BLOCK = 117;// RSA最大加密明文大小
private static final int MAX_DECRYPT_BLOCK = 128;// RSA最大解密明文大小


//公钥 这里只展示部分
public static final String PUBLIC_KEY = "MIGfMA0GCSqG...+wIDAQAB";
//私钥 这里只展示部分
public static final String PRIVATE_KEY = "MIICdwIBADANBg...yhE/3J2I=";

//Base64Utils的decodeFromString方法
public static byte[] decodeFromString(String src) {
    if (src.isEmpty()) {
        return new byte[0];
    }
    return Base64.decode(src, NO_WRAP);
}

/**
 * 公钥加密 RSA/None/PKCS1Padding 填充方式
 *
 * @param data 待加密数据字节数组
 * @param publicKey 公钥
 * @throws Exception
 * @returnn 加密密文
 */
public static byte[] encryptByPublicKeyByPKCS1Padding(byte[] data, String publicKey)
        throws Exception {
    byte[] keyBytes = Base64Utils.decodeFromString(publicKey);
    X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
    KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
    PublicKey publicK = keyFactory.generatePublic(x509KeySpec);
    // 对数据加密
    Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding");
    cipher.init(Cipher.ENCRYPT_MODE, publicK);
    int inputLen = data.length;
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    int offSet = 0;
    byte[] cache;
    int i = 0;
    // 对数据分段加密
    while (inputLen - offSet > 0) {
        if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
            cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
        } else {
            cache = cipher.doFinal(data, offSet, inputLen - offSet);
        }
        out.write(cache, 0, cache.length);
        i++;
        offSet = i * MAX_ENCRYPT_BLOCK;
    }
    byte[] encryptedData = out.toByteArray();
    out.close();
    return encryptedData;
}

/**
 * 私钥解密
 * @param encryptedData 待解密字节数组
 * @param privateKey 私钥
 * @return 明文
 * @throws Exception
 */
public static byte[] decryptByPrivateKeyByPKCS1Padding(byte[] encryptedData, String privateKey) throws Exception {
    byte[] keyBytes = Base64Utils.decodeFromString(privateKey);
    PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
    KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
    Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    cipher.init(Cipher.DECRYPT_MODE, privateK);
    int inputLen = encryptedData.length;
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    int offSet = 0;
    byte[] cache;
    int i = 0;
    // 对数据分段解密
    while (inputLen - offSet > 0) {
        if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
            cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
        } else {
            cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
        }
        out.write(cache, 0, cache.length);
        i++;
        offSet = i * MAX_DECRYPT_BLOCK;
    }
    byte[] decryptedData = out.toByteArray();
    out.close();
    return decryptedData;
}

鸿蒙项目ArkTs(API9)对应的RSA加解密方案

import cryptoFramework from '@ohos.security.cryptoFramework';
import util from '@ohos.util';
import Logger from './Logger'


//公钥 这里只展示部分
export const pubKeyX509Str = "MIGfMA0GCSqG...+wIDAQAB";
//私钥 这里只展示部分
export const priKeyPKCS8Str = "MIICdwIBADANBg...yhE/3J2I=";

export class RSA {
  
  //将字符串转换为Uint8Array
  string2Uint8Array(str: string, len:number = null) {
    let textEncoder = util.TextEncoder.create('utf-8')
  	return textEncoder.encodeInto(str)
  }
  
  //字节流转成可理解的字符串
  uint8ArrayToString(array) {
    let textDecoder = util.TextDecoder.create('utf-8')
    return textDecoder.decodeWithStream(array)
  }
  
  /**
   * RSA分段加密
   * @param value 待加密字符串
   * @returns 
   */
  rsaEncryption(value: string) {
    let base64 = new util.Base64Helper();
    let data = base64.decodeSync(pubKeyX509Str)
    //RSA每次加解密允许的原文长度大小与密钥位数和填充模式等有关,详细规格内容见overview文档
    let plainTextSplitLen = 117;
    let globalEncryptionOutput; //加密输出
    let arrTest = this.string2Uint8Array(value);
    //创建非对称密钥生成器对象
    let asyKeyGenerator = cryptoFramework.createAsyKeyGenerator("RSA1024|PRIMES_2");
    // 创建加密Cipher对象
    let cipherEncryption = cryptoFramework.createCipher("RSA1024|PKCS1");
    let pubKeyBlob = { data: data }
    return asyKeyGenerator.convertKey(pubKeyBlob, null)
      .then(keyPair => {
        return cipherEncryption.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, keyPair.pubKey, null);
      })
      .then(async () => {
        globalEncryptionOutput = [];

        // 将原文按64字符进行拆分,循环调用doFinal进行加密,使用1024bit密钥时,每次加密生成128B长度的密文
        for (let i = 0; i < (arrTest.length / plainTextSplitLen); i++) {
          let tempArr = arrTest.slice(i * plainTextSplitLen, (i + 1) * plainTextSplitLen);
          let tempBlob = { data: tempArr };
          let tempCipherOutput = await cipherEncryption.doFinal(tempBlob);
          globalEncryptionOutput = globalEncryptionOutput.concat(Array.from(tempCipherOutput.data));
        }
        let enStr = base64.encodeToStringSync(new Uint8Array(globalEncryptionOutput))
        Logger.info("加密总长度:" + globalEncryptionOutput.length + "\n生成加密串:\n" + enStr)
        return enStr
      })
      .catch(error => {
        Logger.info(`加密异常, ${error.code}, ${error.message}`);
      })
  }
  
  
  /**
   * RSA分段解密
   * @param value 待解密字符串
   * @returns 
   */
  rsaDecryption(value: string) {
    let base64 = new util.Base64Helper();
    let data = base64.decodeSync(priKeyPKCS8Str)
    // RSA密钥每次加密生成的密文数据长度计算方式:密钥位数/8
    let cipherTextSplitLen = 128;
    //创建非对称密钥生成器对象
    let asyKeyGenerator = cryptoFramework.createAsyKeyGenerator("RSA1024|PRIMES_2");
    // 创建解密Decoder对象
    let cipherDecryption = cryptoFramework.createCipher("RSA1024|PKCS1");
    let priKeyBlob = { data: data }
    return asyKeyGenerator.convertKey(null, priKeyBlob)
      .then(keyPair => {
        return cipherDecryption.init(cryptoFramework.CryptoMode.DECRYPT_MODE, keyPair.priKey, null);
      }).then(async () => {
        let globalCipherOutput = base64.decodeSync(value)
        let len = globalCipherOutput.length
        //解密输出
        let globalDecryptionOutput = new Uint8Array(len);
        let globalOffset = 0
        // 将密文按128B进行拆分解密,得到原文后进行拼接
        for (let i = 0; i < (len / cipherTextSplitLen); i++) {
          let tempBlobData = globalCipherOutput.subarray(i * cipherTextSplitLen, (i + 1) * cipherTextSplitLen);
          let message = new Uint8Array(tempBlobData);
          let tempBlob = { data: message };
          let tempDecodeOutput = await cipherDecryption.doFinal(tempBlob);
          //存入数组 解决边累加边转中文时 字节错乱出现乱码
          globalDecryptionOutput.set(tempDecodeOutput.data, globalOffset)
          //偏移量
          globalOffset += tempDecodeOutput.data.byteLength
        }
        let result = this.uint8ArrayToString(globalDecryptionOutput)
        Logger.info("解密串:cipherAlgName[RSA1024|PKCS1]\n" + result);
        return result
      })
      .catch(error => {
        Logger.info(`解密异常,cipherAlgName[RSA1024|PKCS1] ${error.code}, ${error.message}`);
      })
  }
  
}

网络请求,进行验证

import http from '@ohos.net.http'
import {RSA} from '../tools/RSA'

@Entry
@Component
struct Second {
  @State message: string = "Hi there"
  @State requestStr: string = ''
  @State responseStr: string = ''
  private rsa: RSA = new RSA()
  
  text = {"brandName":"UPOS","lnglat":"116.192321,39.911187","merDetailedAddress":"中国北京市石景山区古城街道古城南街辅路","channel":"guanwang","sign":"5acea2dc35e7a856db260c6ca6f5c993","sysMark":"1","merAreas":"北京市,北京市,石景山区","rpid":"13476A6FC3370A20","version":"63","token":"13670AF3KlZzvhe9Dzju7uDqnc9JrqBv9Ttfy"}
 
  //此处省略build方法
  
  //网络请求
  async requestUpdate() {
    let encrypt = await this.rsa.rsaEncryption(JSON.stringify(this.text))
    this.requestStr = `------请求json------\n{"data": "${encrypt}"}\n------返回json-----\n`
    //每一个httpRequest对应一个Http请求任务,不可复用
    let httpRequest = http.createHttp();
    httpRequest.request(
      "https://www.xxx.com/xxx/xxxx/xxxx.do", {
      method: http.RequestMethod.POST,
      header: {
        'content-type': 'application/json; charset=utf-8',
      },
      extraData: {
        'data': encrypt
      }
    }, async (err, data) => {
      if (!err) {
        //请求成功
        if (data.responseCode === 200) {
          //后台返回json{"ret_code":null,"ret_msg":null,"data":"FGAIlls512...5jplOrXosEIw=="}
          let resultStr = JSON.parse(`${data.result}`)
          this.responseStr = await this.rsa.rsaDecryption(resultStr.data) || "解密失败"
        }
      } else {
        //请求失败
        console.info(`error: ${JSON.stringify(err)}`)
      }
      //请求完毕时,调用destroy方法主动销毁
      httpRequest.destroy()
    })
  }
}

最终结果

鸿蒙ArkTs API9 RSA加解密