关于密码学:密码学基础编码方式消息摘要算法加密算法总结

80次阅读

共计 14983 个字符,预计需要花费 38 分钟才能阅读完成。

字节码转文本的编码方式

在计算机中,无论是内存、磁盘、网络传输,波及到的数据都是以二进制格局来存储或传输的。

每一个二进制位(bit)只能是 0 或 1。二进制位不会独自存在,而是以 8 个二进制位组成 1 个字节(byte)的形式存在,即 1 byte = 8 bit。

字节码无奈间接转为可打印的文本字符,有时想通过文本形式配置、存储、传输一段二进制字节码,比方配置文件、HTML/XML、URL、e-mail 注释、HTTP Header 等仅反对文本的场景下,就须要将二进制字节码转为文本字符串。

二进制字节码转文本字符有很多种形式,最简略的形式是间接用 0 和 1 来示意。然而这样的话,8 个 0/1 字符能力示意 1 个字节,长度太长很不不便。

上面介绍两种更加紧凑的形式:HEX 编码和 Base64 编码。

HEX 编码

HEX 是 16 进制的编码方式,所以又称为 Base16。

如果把一个字节中的二进制数值转为十六进制,应用 0-9 和 a-e(疏忽大小写)这 16 个字符,那每个字符就能够示意 4 个二进制位(因为 2 的 4 次方等于 16),那么仅须要两个可打印字符就能够示意一个字节。

Java 中应用 HEX 编码(依赖 Apache Commons Codec):

String str = "相对论";
byte[] bytes = str.getBytes("UTF-8");

// Hex 编码
String encodeString = Hex.encodeHexString(bytes);
System.out.println(encodeString); // 输入:e79bb8e5afb9e8aeba

// Hex 解码
byte[] decodeBytes = Hex.decodeHex(encodeString);
System.out.println(new String(decodeBytes, "UTF-8")); // 输入:相对论

HEX 编码应用场景十分多。上面介绍几种常见的应用场景:

RGB 色彩码

RGB 色彩通常用 HEX 形式示意。如橘红色能够用 #FF4500 来示意:

.orangered {color: #FF4500;}

RGB 指红 (red) 绿(green)蓝 (blue) 三原色,这三种色彩按不同比例叠加后能够失去各式各样的色彩。三种色彩每种强度取值范畴是 0~255,各须要 1 个字节来示意,共 3 个字节。

用 HEX 编码的示意某种 RGB 色彩,是一个长度为 6 位的字符串(通常还会加上 # 作为前缀,此时长度是 7 位)。例如 #FF4500 示意红绿蓝三原色的强度别离为 255、69、0。

URL 编码

因为 URL 中仅容许呈现字母、数字和一些特殊符号,当 URL 中有汉字,须要通过 URL 编码才能够。

例如百度百科 ” 相对论 ” 的页面 URL 是:https://baike.baidu.com/item/…

其中 %E7%9B%B8%E5%AF%B9%E8%AE%BA 实际上是将 ‘ 相对论 ’ 三个字用 UTF-8 编码后失去 9 个字节,再别离对这 9 个字节应用 HEX 编码并加上 ‘%’ 前缀失去的后果。

IPv6 地址

因为 IPv4 的地址行将面临不够用的问题,取而代之的将会是 IPv6。IPv6 应用了 128 个二进制位的地址,通常会应用 HEX 编码方式来示意,例如:

2001:0db8:0000:0000:0000:ff00:0042:8329

Base64 编码

如果感觉 HEX 编码不够紧凑,那么还有更加紧凑的编码方式:Base64 编码。

Base64 编码共应用了 64 个字符来示意二进制位:26 个大写的 A-Z、26 个小写的 a-z、10 个数字 0-9、2 个特殊符号 + 和 /。这意味着每个字符能够示意 6 个二进制位,因为 64 等于 2 的 6 次方。

因为每个字节是 8 个二进制位,而 Base64 编码每个字符示意 6 个二进制位,那么能够每凑够 3 个字节(即 24 个二进制位),可将其编码为 4 个字符。如果被 base64 编码的原数据字节数不是 3 的倍数,那么会在开端补上 1 或 2 个值为 0 的字节,凑到 3 的倍数后再进行 Base64 编码,编码后会在开端增加 1 或 2 个 = 符号,示意补了多少个字节,这个在解码时会用到。

Java 中应用 Base64 编码:

String str = "相对论";
byte[] bytes = str.getBytes("UTF-8");

// Base64 编码
String encodeString = Base64.getEncoder().encodeToString(bytes);
System.out.println(encodeString); // 输入:55u45a+56K66

// Base64 解码
byte[] decodeBytes = Base64.getDecoder().decode(encodeString);
System.out.println(new String(decodeBytes, "UTF-8")); // 输入:相对论

Base64 编码的应用场景也有很多。例如,因为图片文件不是文本文件,没方法间接写入到 HTML 中,而将图片通过 Base64 编码后的后果是一串文本,能够间接放到 HTML 中:

<img src="data:image/jpg;base64,/9j/4QMZR..." />

须要留神的是,Base64 不是加密算法,有的开发人员把 Base64 当做加密算法来用,这是极其不平安的,因为 Base64 任何人都能够解码,不须要任何密钥。

音讯摘要算法

音讯摘要算法(Message-Digest Algorithm),又称为明码散列函数(cryptographic hash function (CHF)),能够将任意长度的字节码数据通过哈希算法计算出一个固定大小的后果。罕用的音讯摘要算法有 MD5、SHA-1、SHA-2 系列(包含 SHA-256、SHA-512 等)。

以 MD5 为例,对任意一个数据进行 MD5 运算,后果是一个 128 个二进制位(16 个字节)的哈希值。而咱们日常看到的 32 位 MD5 字符串,实际上是对 128 个二进制位的哈希值进行 HEX 编码后失去的后果。

例如,当应用 MD5 对 “ 相对论 ” 这个字符串进行运算,失去一个 32 位字符的 MD5 值,实际上是通过以下 3 个步骤(以下代码依赖 Apache Commons Codec):

String str = "相对论";
// 1. 将字符串通过 UTF-8 编码转为字节数组
byte[] bytes = str.getBytes("UTF-8");
// 2. 对原始数组进行 MD5,失去一个 128 个二进制位(16 个字节)的哈希值
byte[] md5Bytes = DigestUtils.md5(bytes);
// 3. 将 128 位的哈希值 HEX 编码,失去一个长度为 32 的字符串
String md5Hex = Hex.encodeHexString(md5Bytes);
System.out.println(md5Hex); // 输入:fa913fb181bc1a69513e3d05a367da49

下面的代码仅仅是为了更清晰的看到计算一个字符串 MD5 值的整个过程。理论开发中能够应用更加便捷的 API,将下面的 3 个步骤合为 1 步:

String str = "相对论";
// 应用默认的 UTF-8 编码将字符串转为字节数组计算 MD5 后再进行 HEX 编码
String md5Hex = DigestUtils.md5Hex(str);
System.out.println(md5Hex); // 输入:fa913fb181bc1a69513e3d05a367da49

除此之外,Apache Commons Codec 中的 DigestUtils 还提供了 SHA-1、SHA-256、SHA-384、SHA-512 等音讯摘要算法。

音讯摘要算法有以下特点:

  • 雷同的音讯通过音讯摘要算法计算失去的后果总是雷同的。
  • 不同的音讯通过音讯摘要算法计算失去的后果要尽可能保障是不同的。如果两个不同的数据音讯摘要后的后果雷同,也就是产生了哈希碰撞,哈希碰撞呈现的概率越大,那么这个音讯摘要算法就越不平安。
  • 不可逆,无奈通过哈希后果反向推算出原始数据。所以,咱们个别认为 音讯摘要算法并不算是加密算法,因为它无奈解密。另外,这里的不可逆是指运算不可逆,然而攻击者通常会应用穷举法或彩虹表来找到哈希值对应的原始数据。

上面列举一些典型的音讯摘要算法的应用场景:

  • 对用户的登录明码应用音讯摘要算法失去哈希值后再存储到数据库,即便数据库被黑客攻击,拿到所有的数据,也很难取得明码的原始值。这绝对明文存储明码来说更加平安。当然,间接应用哈希值存储也是不平安的,特地是对于一些弱明码,黑客能够通过彩虹表轻松的查到对应的原始值。所以通常不会间接存储哈希值,而是通过一些解决,例如加盐、HMAC 等形式。
  • 比照两个文件是否统一,只须要比照两个文件的音讯摘要是否统一即可,无需按字节一个个去比照。例如百度网盘已经就是用文件的 MD5 来判断新上传的文件是否已存在,如果曾经存在则不须要反复上传和存储,达到节俭空间的目标。
  • 用于数字签名(Digital Signature),这个在本文后续会介绍。

在安全性要求比拟高的场景下,MD5、SHA-1 目前都曾经不倡议应用了,当初用的比拟多的是 SHA-2 系列算法。

HMAC

HMAC 全称是散列音讯认证码(Hash-Based Message Authentication Code),它在音讯摘要算法的根底上,加上了一个密钥(secret key)。

例如 HMAC-SHA256 就是在 SHA-256 算法根底上加了一个密钥。以下为代码示例(依赖 Apache Commons Codec):

String str = "相对论";
String key = "12345678"; // 密钥
HmacUtils hmacUtils = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, key.getBytes("UTF-8"));
String result = hmacUtils.hmacHex(str.getBytes("UTF-8"));
System.out.println(result); // 输入:3bd7bbf58159a6d0bff846016b346a617a588fc1e9c43ebbdf38be53d3fc455a

绝对于间接应用音讯摘要算法,应用 HMAC 劣势在于,它能够对音讯进行真实性(authenticity)和完整性(integrity)验证。

只有密钥没有泄露,那么只有持有密钥才能够计算和验证原始数据哈希值。攻击者在没有密钥的前提下,无奈发送伪造的音讯,也无奈篡改音讯。

HMAC 可用于接口认证。例如一个裸露在网络环境中的 HTTP 接口,如果想要对调用方进行认证,能够将密钥发放给调用方,要求调用方调用接口时,给所有申请参数应用密钥通过 HMAC 计算一个签名,被调用方验证签名,就能够保障申请参数的真实性和完整性。

另外,HMAC 因为在计算哈希值时增加了密钥,绝对于间接应用音讯摘要算法,更加不容易被穷举法、彩虹表破解,用户明码通过 HMAC 后保留更加平安。

JWT 中的 HMAC

HMAC 的一个典型的利用场景就是 JWT。JWT 全称是 JSON Web Token。

传统的认证形式个别会将认证用户信息保留在服务端,而 JWT 间接将认证用户信息发放给客户端保留。既然 JWT 保留在客户端,那么任何人都能够伪造或篡改。如何解决这个问题,其中一种形式就是服务端会对 JWT 的 token 应用 HMAC 进行签名,并将签名也放在 token 开端。下次客户端带上 JWT 申请时,服务端再验证签名是否正确。只有密钥不泄露,就能够保障 token 的真实性和完整性。

JWT token 分为三个局部:

  • Header:头部,指定签名算法
  • Payload:蕴含 token 次要传输的信息,这一部分能够蕴含用户信息,例如用户名等
  • Signature:签名,计算形式如下(secret 即密钥):

    HMACSHA256(base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret)

最终对这三个局部 Base64 编码后组合为 JWT 的 token:

加密算法

加密算法分为对称加密算法和非对称加密算法:

  • 对称加密算法(symmetric-key cryptography):加密和解密时应用雷同的密钥。最罕用的是 AES 算法。
  • 非对称加密算法(asymmetric-key cryptography):加密和解密应用不同的密钥,例如公钥加密的内容只能用私钥解密,所以又称为公钥加密算法(public-key cryptography)。应用最宽泛的是 RSA 算法。

对称加密算法

常见的对称加密算法有 DES、3DES、AES,其中 DES 和 3DES 规范因为安全性问题,曾经逐步被 AES 取代。

AES 有多种工作模式(mode of operation)和填充形式(padding):

  • 工作模式:如 ECB、CBC、OFB、CFB、CTR、XTS、OCB、GCM,不同的模式参数和加密流程不同。
  • 填充形式:因为 AES 是一种区块加密(block cipher)算法,加密时会将原始数据按大小拆分成一个个 128 比特(即 16 字节)区块进行加密,如果须要加密的原始数据不是 16 字节的整数倍时,就须要对原始数据进行填充,使其达到 16 字节的整数倍。罕用的填充形式有 PKCS5Padding、ISO10126Padding 等,另外如果能保障待加密的原始数据大小为 16 字节的整数倍,也能够抉择不填充,即 NoPadding。

在理论工作中,须要跨团队跨语言对数据加密解密,经常出现应用一个语言加密后,另一个语言无奈解密的状况。这个别都是两边抉择的工作模式和填充形式不统一导致的。

上面的代码以 ECB 模式联合 PKCS5Padding 填充形式为例,对数据进行加密和解密:

public static byte[] encryptECB(byte[] data, byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"));
    byte[] result = cipher.doFinal(data);
    return result;
}

public static byte[] decryptECB(byte[] data, byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"));
    byte[] result = cipher.doFinal(data);
    return result;
}

public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
    String data = "Hello World"; // 待加密的明文
    String key = "12345678abcdefgh"; // key 长度只能是 16、25 或 32 字节

    byte[] ciphertext = encryptECB(data.getBytes(), key.getBytes());
    System.out.println("ECB 模式加密后果(Base64):" + Base64.getEncoder().encodeToString(ciphertext));

    byte[] plaintext = decryptECB(ciphertext, key.getBytes());
    System.out.println("解密后果:" + new String(plaintext));
}

输入:

ECB 模式加密后果(Base64):bB0gie8pCE2RBQoIAAIxeA==
解密后果:Hello World

下面的 ECB 模式尽管简略易用,然而安全性不高。因为该模式对每个 block 进行独立加密,会导致同样的明文块被加密成雷同的密文块。下图就是一个很好的例子:

在 CBC 模式中,引入了初始向量(IV,Initialization Vector)的概念,用于解决 ECB 模式的问题。

上面是 CBC 模式联合 PKCS5Padding 填充形式的代码示例,加密解密时相比 ECB 模式多了一个初始向量 iv 参数:

public static byte[] encryptCBC(byte[] data, byte[] key, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
    byte[] result = cipher.doFinal(data);
    return result;
}

public static byte[] decryptCBC(byte[] data, byte[] key, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
    byte[] result = cipher.doFinal(data);
    return result;
}

public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {
    String data = "Hello World"; // 待加密的原文
    String key = "12345678abcdefgh"; // key 长度只能是 16、25 或 32 字节
    String iv = "iviviviviviviviv"; // CBC 模式须要用到初始向量参数

    byte[] ciphertext = encryptCBC(data.getBytes(), key.getBytes(), iv.getBytes());
    System.out.println("CBC 模式加密后果(Base64):" + Base64.getEncoder().encodeToString(ciphertext));

    byte[] plaintext = decryptCBC(ciphertext, key.getBytes(), iv.getBytes());
    System.out.println("解密后果:" + new String(plaintext));
}

输入:

CBC 模式加密后果(Base64):K7bSB51+KxfqaMjJOsPAQg==
解密后果:Hello World

AES 应用十分宽泛,能够说只有上网,无论是应用手机 APP 还是 Web 利用,简直都离不开 AES 加密算法。目前大部分网站,包含手机 APP 后端接口,都曾经应用 HTTPS 协定,而 HTTPS 在数据传输阶段大多都是应用 AES 对称加密算法。

然而,以 AES 为代表的的对称加密算法面临一个问题,就是如何平安的传输密钥。网络中产生数据交换的单方,须要用同一个密钥进行加密和解密,密钥一旦裸露,传输的内容就不再平安。密钥自身如果须要传输,如何保障平安?对于这个问题,就须要用到非对称加密算法。

非对称加密算法

1977 年,Rivest、Shamir、Adleman 设计了 RSA 非对称加密算法,并以此取得了 2002 年的图灵奖(计算机领域的国内最高奖项,被誉为 ” 计算机界的诺贝尔奖 ”)。至今,RSA 算法始终是最广为应用的非对称加密算法。

RSA 有两个密钥:公钥(public key)和私钥(private key)。

公钥能够齐全公开,任何人都能够获取到。私钥是公有的,要保障不能被泄露进来。

公钥加密的内容,只有私钥能够解密。私钥加密的内容,也只有公钥能够解密。

基于以上规定,RSA 有两种不同的用法:

  • 公钥加密,私钥解密:服务端把公钥公开进来,客户端拿到公钥,把想要传输给服务端的数据通过公钥加密后传输,那么这个数据只有服务端可能解密,因为只有服务端领有私钥,其余任何中间人即便在传输过程中拿到数据,既不能解密,也无奈篡改。
  • 私钥签名,公钥验证签名:内容发布者将公布的内容用音讯摘要算法(如 SHA-256)计算哈希值,再用私钥加密哈希值,失去一个签名,并将签名加在公布内容中一起公布,其他人失去这个内容后,能够用公开的公钥解密签名失去哈希值,再比照这个哈希值和内容生成的哈希值是否统一,来保障这份内容没有被篡改过。

    因为只是验证数据的真实性完整性,所以无需对整个内容进行加密,仅需对内容的哈希值加密即可验证,所以通常会联合音讯摘要算法。例如 SHA256 with RSA 签名,就是先用 SHA-256 计算出哈希值,再用 RSA 私钥加密。

下面说到的私钥加密、公钥解密只是实践上成立,实际上不会间接这样用,而是只用于签名。因为一段私钥加密的数据,解密的公钥是公开的,意味着谁都能够解密,这样加密就没有任何意义了。

接下来通过 Java 代码来体验一下 RSA 算法。

首先,须要生成一对公钥和私钥。上面通过 openssl 命令来生成一对公钥和私钥:

# 创立一个 PKCS#8 格局 2048 位的私钥
openssl genpkey -out private_key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
# 通过私钥生成公钥
openssl pkey -in private_key.pem -pubout -out public_key.pem

生成的公钥和私钥是 Base64 编码的文本文件,能够间接用文本编辑器关上。拷贝到上面的代码中,能够验证公钥加密、私钥解密,以及私钥签名、公钥验证签名:

public static void main(String[] args) throws Exception {
    String publicKeyBase64 = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0XYlulDsTzDWUb6X66Ia\n" +
            "giSn1dKriHvLHYth9hCcaGomdeIQahGnxzE1o76slEyS2HZ164QHqx8Za+LuT6IV\n" +
            "yLhU/ZNLWAZABe/sdNEkhti6vSSOdJE43KS4UVADeSgtN+7uXDuVgm35EPWZjkfV\n" +
            "5hiRX4nT5ALr1niyi1Ax4BWWyG4qX00n1HzY8MvoyiLdNob71qB+amjUNy9bDhcz\n" +
            "CDWtgA/ywOYU5Ec6vMgYfbAXPKGWwo318rS3UH8QtsO8iGcQbZ76q05LNEL8G3fo\n" +
            "0Kssj4fjrVGwSsyGztRRMLfGkW/hOPCDj82+D6dGQlGB3gyB7P1xVbkD67FujQA/\n" +
            "jwIDAQAB";
    String privateKeyBase64 = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDRdiW6UOxPMNZR\n" +
            "vpfrohqCJKfV0quIe8sdi2H2EJxoaiZ14hBqEafHMTWjvqyUTJLYdnXrhAerHxlr\n" +
            "4u5PohXIuFT9k0tYBkAF7+x00SSG2Lq9JI50kTjcpLhRUAN5KC037u5cO5WCbfkQ\n" +
            "9ZmOR9XmGJFfidPkAuvWeLKLUDHgFZbIbipfTSfUfNjwy+jKIt02hvvWoH5qaNQ3\n" +
            "L1sOFzMINa2AD/LA5hTkRzq8yBh9sBc8oZbCjfXytLdQfxC2w7yIZxBtnvqrTks0\n" +
            "Qvwbd+jQqyyPh+OtUbBKzIbO1FEwt8aRb+E48IOPzb4Pp0ZCUYHeDIHs/XFVuQPr\n" +
            "sW6NAD+PAgMBAAECggEABT96joJ8iTdmB0JJOCQlmeElO1w94/uGCUV2vN2JrawL\n" +
            "LqTtWFr84ya+e03JsSWCAF5ncfEq6AStdGCJLAGZnh/QMVJBbwEpFXz/ZaXfzmkb\n" +
            "tKV31D/XNuABpjfk/mIdT+tymWj8w/nRZbVhlYkDOPKgoc4oOuw/0G3Ru1/VABI+\n" +
            "yulNx93A/JNFGk3Bkm4E7jRWyl0BkAqAX2BZkFbXG/u3Jc0eYXrG74JfMH+MEihG\n" +
            "GDMSpBKNyX5zWkUT6XxpG82t2erHPWYEoNSoFzAUu+7rZ4ECEXxazAQclEHTkR3r\n" +
            "duUZ/XF0GL1WB0GC7+qvV/Z0gxjXuwG9oToFO/0MQQKBgQDu4DuTPWcYwSWY0R1f\n" +
            "qZUOuYRwD+5OQnJMIlKAD32QmvYT/jnvigjss5Qf1IUwf1UMynj2FnVF4D7L+kvq\n" +
            "O7LzYvHAeDQwZGGt2xWBlqjfhumlfBqfklkkqUiH2A5DvfvtbX/kkiY3n9C+oYZp\n" +
            "2ejiOtSC+NqQeB74TluxroEkvwKBgQDgehynybpFl4KkmDhgj++BH5RR+xzXIChb\n" +
            "gtIbbspdE1EyXy7Z9iNAJ8PVjHkSwh8iEfAO4EuJFnonF8UNIsWLr3gsKbQytRxR\n" +
            "cewqaBhTL54Vgl5dmODNrYjkZva5HHDsCLioYGgljdrj5e/gPSAWBrgT6kI+HypQ\n" +
            "/5xyp+KJMQKBgQCMxut1P8eliBa/M+YqvYdR8TVC0bCwwGoZwlR6kiZ+9UQ2zimY\n" +
            "qPHPhZmzFI0V4sTdz+lvphahAqIfljftKBezZklxE6Y2KsKCMk4/W+nUKe9Cjpwm\n" +
            "FJqih31uSX9Gnw18hH7N1u/c8juUTR8o/LpJsUASm9Q7Nf+SeKODWINVgwKBgDEx\n" +
            "UXpLsPBzRYQAf8pZgKkRXJWirC1QtMdpIdY1L0+6Xf7l8QR+9janADmaMSY1OFFl\n" +
            "EPCRorwGGvraMKqyRgxYhcNX2E+MdQo8Jv8cFMiWFNSt3zQvvoQUVX2IOuVSIET5\n" +
            "nE354pjoP2HWD/1aJ9/r1Qc4PRAUEFfzzDssI27hAoGAOsYKtvW6iRn/WVduIRcy\n" +
            "UtBRHHX0U16zGv+I7nOOBIYK5Uan6AjgzG2MfPOBj3cUhMMBDPfVg1cTbonw5Y8F\n" +
            "nSO4VLOtqKy0BRxCIUFqltJXUmj1zAJs84IweCBQ3un/OLVUMgE7qGtaIQy2PBsy\n" +
            "M8mwuUjo3Fu7l11E2Vgz/qY=";

    // Base64 解码
    byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64.replace("\n", ""));
    byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64.replace("\n", ""));

    // 生成 PublicKey(公钥)和 PrivateKey(私钥)对象
    PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyBytes));
    PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));

    // 待加密、签名的原文
    String data = "Hello World";

    // 公钥加密,私钥解密
    byte[] ciphertext = encrypt(data.getBytes(), publicKey);
    System.out.println("RSA 公钥加密后果(Base64):" + Base64.getEncoder().encodeToString(ciphertext));
    byte[] plaintext = decrypt(ciphertext, privateKey);
    System.out.println("RSA 私钥解密后果:" + new String(plaintext));

    // 私钥签名,公钥验证签名
    byte[] signature = sign(data.getBytes(), privateKey);
    System.out.println("RSA 私钥签名后果(Base64):" + Base64.getEncoder().encodeToString(signature));
    boolean verifySuccess = verify(data.getBytes(), signature, publicKey);
    System.out.println("RSA 公钥验证签名后果:" + verifySuccess);
}

/**
 * 公钥加密
 */
public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception {Cipher cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.ENCRYPT_MODE, publicKey);
    byte[] result = cipher.doFinal(data);
    return result;
}

/**
 * 私钥解密
 */
public static byte[] decrypt(byte[] data, PrivateKey privateKey) throws Exception {Cipher cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.DECRYPT_MODE, privateKey);
    byte[] result = cipher.doFinal(data);
    return result;
}

/**
 * 私钥签名,应用 SHA256 with RSA 签名
 */
public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception {Signature signature = Signature.getInstance("SHA256withRSA");
    signature.initSign(privateKey);
    signature.update(data);
    byte[] result = signature.sign();
    return result;
}

/**
 * 公钥验证签名
 */
public static boolean verify(byte[] data, byte[] sign, PublicKey publicKey) throws Exception {Signature signature = Signature.getInstance("SHA256withRSA");
    signature.initVerify(publicKey);
    signature.update(data);
    return signature.verify(sign);
}

输入:

RSA 公钥加密后果(Base64):zoY6KM/RdCjAs7upJ9SIwqfXsSn3hAPu/z/ZPHbKgWN6+X0PpyVJVYT8jacEkzB7S2sJe/wLkO2TqXB2gqvL1AuDRgepVlxV2f6Uwx4DxM2/5RE0fAdTiICV5JEEIw81oLix0GGQ7nLjOhJxN9LaTJ2cXtwgR8gUtLtJ0tdWrxSMuN8FHLA45Nv8Ea1EAUQCvfanYZ2L39l++3/zBdg2wYQwCE6XGFnWnayUsGKYjC7JIufnq5f9VDL/kguLKceLmeTHqq31ccRTOQyhuoZjHCsbfXPlW2AT9ejgAcXy7LkXhYCfma50DBM+KUCfC4YrKBg6wKRqdZee90ZPcUKTkw==
RSA 私钥解密后果:Hello World
RSA 私钥签名后果(Base64):AbP5zSV/qvkF8fCseVkEaZMscvznQBUDtO3g0U/FIXVmzeR6WXFwPsMd3cC3oCHtnnqsL/aRQrpW6pHU6EzSJ5w6FgY6kD4kWREq9f8LOnyQm7CoS6CK0tUiAjIgG16rtmS+oPbG+mYaZkLzo1Cpkpz2MzuMMbWNivvXRMbj3wLiXyIMqUefawipvm+GPwrWRxesRot2sGtuZcxtMMZs3NHpJ0CXV/mQlYJWEzIiHUY4mqfqpMDL/djPf9td74ABpjk38O6r1Jt75TLnMvkwRdh7pHBQLZ0Tn/6Vx2cVD2D+sE9BuhinO66B6I0QOGVcl3a5C2whp+85zEovvdGlSg==
RSA 公钥验证签名后果:true

目前随处可见的 HTTPS 协定,是基于 SSL/TLS 协定的。在 SSL/TLS 协定中,建设加密的传输通道前,首先有一个握手过程。在握手过程中,客户端会生成一个随机值,并应用公钥加密后传给服务端。这个随机值用于生成对称加密算法的密钥,仅有服务端的私钥能够解密,任何第三方都无奈解密,这就解决了后面所说到的对称加密算法密钥传输过程中的平安问题。而握手胜利后的通信阶段,则应用对称加密算法进行通信。因为非对称加密算法更加简单,绝对于对称加密算法来说效率不高,不适宜用来做大量数据的加密解密。

另外,SSL/TLS 中用到的数字证书(digital certificate),为了避免伪造,也会由 CA 机构进行数字签名。目前大多数 HTTPS 网站应用的数字证书都是应用 SHA256 with RSA 签名。

例如,在浏览器上关上 https://xxgblog.com/,点击地址栏左侧的小锁按钮,查看网站应用的证书,其数字签名算法就是 SHA256 with RSA:

正文完
 0