数字信封说明

数字信封是一种综合利用对称加密和非对称加密两种技术优势来保证数据安全传输的技术。

数字信封的作用

  • 保证数据的机密性

    即使数据在传输过程中被截获,攻击者由于没有接收方的私钥,无法解密出对称密钥,因而也无法解密原始数据。

  • 解决对称密钥的安全分发问题

    这是数字信封最核心的作用。对称加密虽然快,但如何将密钥安全地告诉对方是个难题。数字信封利用非对称加密的安全性,完美地将对称密钥传递给了指定的接收方。

  • 提高加密效率

    它结合了两种加密方式的优点:用高效的对称加密处理大数据,用安全的非对称加密处理小数据(密钥)。在安全性和性能之间取得了最佳平衡。

  • 保证数据的完整性

    数字信封和数字签名一同使用。发送方先用私钥对数据生成签名,再执行数字信封的流程。接收方验证签名即可确认数据在传输过程中未被篡改,并验证发送方身份。

数字信封使用

数字信封格式为16位随机密钥,由数字和字母组成

签名算法 对应的敏感字段加密算法 对应的数字信封加密算法
RSA AES/CBC/NoPadding RSAES-OAEP(RSA公钥加密、OAEP 填充)
SM2 SM4/ECB/PKCS5Padding SM2 公钥加密

敏感字段加密和数字信封加密最终均转为16进制大写形式传输

数字信封加密算法

RSA公钥加密算法

数字信封加密使用的RSA公钥加密算法。加密算法使用的填充方案,我们使用了相对更安全的RSAES-OAEP(Optimal Asymmetric Encryption Padding)。

RSAES-OAEP在各个编程语言中的模式值为:

  • OpenSSL ,padding设置为RSA_PKCS1_OAEP_PADDING

  • Java,使用Cipher.getinstance(RSA/ECB/OAEPWithSHA-1AndMGF1Padding)

  • PHP,padding设置为OPENSSL_PKCS1_OAEP_PADDING

  • .NET,fOAEP设置为true

  • Node.js,padding设置为crypto.constants.RSA_PKCS1_OAEP_PADDING

  • GO,使用EncryptOAEP

开发者应当使用宝付公钥,对上送的数字信封进行加密。这样只有拥有私钥的宝付才能对密文进行解密,从而保证了数字信封的机密性。

另一方面,宝付使用 商户证书中的公钥对下行的数字信封进行加密。开发者应使用商户私钥对下行的数字信封的密文进行解密。

国密加密算法

SM2椭圆曲线公钥加密算法

数字信封加密示例

上行时,开发者应当使用宝付公钥,对上送的数字信封进行加密传输。
详见整体说明里的数字信封字段dgtl_envlp

同理 下行时,如果有敏感字段 宝付会用商户的公钥加密数字信封,数字信封在Http请求头的 Baofu-Dgtl-Envlp里

示例代码

RSA公钥加密算法

public static String rsaEncryptOAEP(String message, PublicKey publicKey)
throws IllegalBlockSizeException, IOException {
  try {
    Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
    cipher.init(Cipher.ENCRYPT_MODE, publicKey);
    byte[] data = message.getBytes("utf-8");
    byte[] cipherdata = cipher.doFinal(data);
    return Base64.getEncoder().encodeToString(cipherdata);
  } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
    throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
  } catch (InvalidKeyException e) {
    throw new IllegalArgumentException("无效的公钥", e);
  } catch (IllegalBlockSizeException | BadPaddingException e) {
    throw new IllegalBlockSizeException("加密原串的长度不能超过214字节");
  }
}

SM2公钥加密算法

    private static String[] ECC_PARAM = {
            "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF",
            "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC",
            "28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93",
            "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123",
            "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7",
            "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0"
    };

    private static final ECCurve ECC_CURVE;
    private static final ECDomainParameters ECC_BC_SPEC;
    private static final ECKeyPairGenerator ECC_KEY_PAIR_GENERATOR;

    static {
        BigInteger ecc_p = new BigInteger(ECC_PARAM[0], 16);
        BigInteger ecc_a = new BigInteger(ECC_PARAM[1], 16);
        BigInteger ecc_b = new BigInteger(ECC_PARAM[2], 16);
        BigInteger ecc_n = new BigInteger(ECC_PARAM[3], 16);
        BigInteger ecc_gx = new BigInteger(ECC_PARAM[4], 16);
        BigInteger ecc_gy = new BigInteger(ECC_PARAM[5], 16);

        ECC_CURVE = new ECCurve.Fp(ecc_p, ecc_a, ecc_b, ecc_n, BigInteger.ONE);
        ECPoint ecc_point_g = ECC_CURVE.createPoint(ecc_gx, ecc_gy);
        ECC_BC_SPEC = new ECDomainParameters(ECC_CURVE, ecc_point_g, ecc_n);
        ECKeyGenerationParameters generationParameters =
                new ECKeyGenerationParameters(ECC_BC_SPEC, new SecureRandom());
        ECC_KEY_PAIR_GENERATOR = new ECKeyPairGenerator();
        ECC_KEY_PAIR_GENERATOR.init(generationParameters);
    }

    public static String encrypt(byte[] publicKey, byte[] data) throws CryptoException{
        return encrypt(ECC_CURVE.decodePoint(publicKey), data);
    }

    public static byte[] encrypt(ECPoint ecPoint, byte[] data) throws CryptoException{
        try {
            ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(ecPoint, ECC_BC_SPEC);
            ParametersWithRandom parametersWithRandom = new ParametersWithRandom(publicKeyParameters);
            SM2Engine sm2Engine = new SM2Engine();
            sm2Engine.init(true, parametersWithRandom);
            return sm2Engine.processBlock(data, 0, data.length);
        } catch (InvalidCipherTextException e) {
            throw new CryptoException("SM2公钥加密异常", e);
        }
    }

数字信封解密示例

RSA公钥加密解密

public static String rsaDecryptOAEP(String ciphertext, PrivateKey privateKey)
throws BadPaddingException, IOException {
  try {
    Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
    cipher.init(Cipher.DECRYPT_MODE, privateKey);
    byte[] data = Base64.getDecoder().decode(ciphertext);
    return new String(cipher.doFinal(data), "utf-8");
  } catch (NoSuchPaddingException | NoSuchAlgorithmException e) {
    throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
  } catch (InvalidKeyException e) {
    throw new IllegalArgumentException("无效的私钥", e);
  } catch (BadPaddingException | IllegalBlockSizeException e) {
    throw new BadPaddingException("解密失败");
  }
}

国密解密

public static byte[] decrypt(BigInteger privateKey, byte[] data) throws CryptoException{
        try {
            ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKey, ECC_BC_SPEC);
            SM2Engine sm2Engine = new SM2Engine();
            sm2Engine.init(false, privateKeyParameters);
            return sm2Engine.processBlock(data, 0, data.length);
        } catch (InvalidCipherTextException e) {
            throw new CryptoException("SM2私钥解密异常", e);
        }
    }