JavaRush /Java 博客 /Random-ZH /Java 密码学架构:初次介绍
Viacheslav
第 3 级

Java 密码学架构:初次介绍

已在 Random-ZH 群组中发布
数据交换的安全性是现代应用程序最重要的属性之一。自古以来,人们就想出了巧妙的方法,随着人类的发展,密码学成为一门完整的科学。当然,Java 并没有袖手旁观,而是为开发人员提供了 Java 密码体系结构 (JCA)。这篇评论应该让您初步了解它是如何工作的。

前言

我建议回到过去。摆在我们面前的是古罗马。在我们面前的是盖乌斯·朱利叶斯·凯撒,他向他的指挥官们发送了一条信息。我们来看看这条消息的内容:
Java 密码学架构:初次介绍 - 2
这可能意味着什么:"ЕСКЕУГЬГМХИФЯ Е УЛП"?我们打开Java在线编译器,例如:repl.it
class Main {
  public static void main(String[] args) {
    String code = "ЕСКЕУГЬГМХИФЯ Е УЛП";
    for (char symbol : code.toCharArray()) {
      if (symbol != ' ') {
        symbol = (char) (symbol - 3);
      }
      System.out.print(symbol);
    }
  }
}
摆在我们面前的是凯撒密码最简单的实现。根据古罗马历史学家苏托尼乌斯(Suetonius)题为《十二位凯撒的传记》的著作,凯撒正是通过这种方式向他的将军们加密信息。这是对密码学之类的使用的最古老的参考之一。“密码学”一词源自古希腊语“隐藏”和“写入”,即 这是隐私技术的科学。Java 有自己的密码学支持,称为Java 密码学体系结构(JCA)。描述可以在Oracle的官方文档——《Java Cryptography Architecture (JCA)》中找到。我建议您看看我们通过 JCA 获得了哪些机会。
Java 密码学架构:初次介绍 - 3

J.C.A.

正如我们之前了解到的,Java 提供了 Java 密码体系结构 (JCA) 来处理密码学。该架构包含一个 API(即一组特定的接口)和提供者(实现它们):
Java 密码学架构:初次介绍 - 4
正如文档所述,“ Java 平台包含许多内置提供程序”。也就是说,Java 平台提供了一组内置的提供程序,可以根据需要进行扩展。你可以自己看看:
import java.security.Provider;
import java.security.Security;
class Main {
  public static void main(String[] args) {
    Provider[] providers = Security.getProviders();
    for (Provider p : providers) {
      System.out.println(p.getName());
    }
  }
}
注册第三方提供商非常容易。例如: Security.addProvider(new BouncyCastleProvider()); 此示例连接了最著名的提供商之一 - BouncyCastle。但在本次审查中,我们将仅使用基本工具,而不使用第三方库。我们的主要文档:“ Java Cryptography Architecture (JCA) ”。了解 JCA 的工作原理将帮助您更轻松地了解积极使用该 JCA 的技术。例如:HTTPS(请参阅“从 HTTP 到 HTTPS ”)。
Java 密码学架构:初次介绍 - 5

信息摘要

JCA文档中首先提到的是MessageDigest。一般来说,俄语中的“摘要”是相同的——“摘要”的意思相当于“摘要”。但在密码学中,摘要是哈希和。你也可以很容易记住,在英语中Digest也可以翻译为摘要。更多详细信息可以在 JCA 文档的“ MessageDigest ”部分中找到。正如文档所述,MessageDigest 生成一个固定大小的结果,称为摘要或哈希。散列是一种单向函数,即 如果我们对某些内容进行散列,那么从结果(即散列)中我们无法获得原始来源。但如果相同的对象被散列(例如,相同字符的字符串),那么它们的散列必须匹配。如文档中所述,此类哈希有时也称为数据的“校验和”或“数字指纹”。可以使用不同的算法来执行散列。可用的算法可以查看文档《Java Cryptography Architecture Standard Algorithm Name Documentation for JDK 8》。让我们进行散列并将散列打印到控制台:
import javax.xml.bind.DatatypeConverter;
import java.security.*;
public class Main {
  public static void main(String[] args) {
    try {
      MessageDigest digester = MessageDigest.getInstance("SHA-512");
      byte[] input = "Secret string".getBytes();
      byte[] digest = digester.digest(input);
      System.out.println(DatatypeConverter.printHexBinary(digest));
    } catch (NoSuchAlgorithmException e) {
      throw new IllegalStateException(e);
    }
  }
}
例如,在存储密码时,哈希可能很有用。由于可以根据之前保存的哈希值检查输入密码的哈希值。如果哈希值匹配,则密码也匹配。为了更安全的散列,使用了称为“盐”的概念。Salt 可以使用SecureRandom类来实现。在执行digest方法之前,我们来描述一下添加“盐”:
byte[] salt = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(salt);
digester.update(salt);
但哈希是一种单向函数。但是如果您希望能够加密和解密怎么办?
Java 密码学架构:初次介绍 - 6

对称密钥密码学

对称加密是使用相同密钥进行加密和解密的加密。为了使用对称加密,我们需要一个密钥。为了获得它,我们使用KeyGenerator。此外,我们还需要一个表示密码(Cipher)的类。正如 JCA 文档中“创建密码对象”部分所述,要创建密码,您不仅需要指定算法,还需要在行中指定“转换”。转换描述如下所示:“算法/模式/填充”:
  • 算法:这里我们看一下“密码(加密)算法”的标准名称。推荐使用AES。
  • 模式:加密模式。例如:ECB 或 CBC(我们稍后会讨论这个)
  • 缩进/分割:每个数据块都单独加密。该参数决定多少数据算作 1 个块。
例如,采用以下转换:"AES/ECB/PKCS5Padding"。即加密算法为AES,加密方式为ECB(Electronic Codebook的缩写),块大小为PKCS5Padding。PKCS5Padding 表示一个块的大小为 2 字节(16 位)。电子密码本加密模式涉及每个块的顺序加密:
Java 密码学架构:初次介绍 - 7
代码中可能如下所示:
import javax.xml.bind.DatatypeConverter;
import javax.crypto.*;
import java.security.Key;
public class Main {
  public static void main(String[] args) throws Exception {
    String text = "secret!!secret!!secret!!secret!!";
    // Generate new key
    KeyGenerator keygen = KeyGenerator.getInstance("AES");
    keygen.init(256);
    Key key = keygen.generateKey();
    // Encrypt with key
    String transformation = "AES/ECB/PKCS5Padding";
    Cipher cipher = Cipher.getInstance(transformation);
    cipher.init(Cipher.ENCRYPT_MODE, key);
    byte[] encrypted = cipher.doFinal(text.getBytes());
    System.out.println(DatatypeConverter.printHexBinary(encrypted));
    // Decrypt with key
    cipher.init(Cipher.DECRYPT_MODE, key);
    String result = new String(cipher.doFinal(encrypted));
    System.out.println(result);
  }
}
如果我们执行,我们将期望看到重复,因为 我们指定了 32 个字符。这些字符组成 2 个 16 位块:
Java 密码学架构:初次介绍 - 8
为了避免在这种情况下重播,您应该使用另一种模式 - 密码块链接 (CBC)。该模式引入了初始化向量的概念(由 IvParameterSpec 类表示)。也得益于这种模式,生成最后一个块的结果将用于生成下一个块:
Java 密码学架构:初次介绍 - 9
现在让我们将其写在代码中:
import javax.xml.bind.DatatypeConverter;
import javax.crypto.*;
import java.security.*;
import javax.crypto.spec.IvParameterSpec;
public class Main {
  public static void main(String[] args) throws Exception {
    // Initialization Vector
    SecureRandom random = SecureRandom.getInstanceStrong();
    byte[] rnd = new byte[16];
    random.nextBytes(rnd);
    IvParameterSpec ivSpec = new IvParameterSpec(rnd);
    // Prepare key
    KeyGenerator keygen = KeyGenerator.getInstance("AES");
    keygen.init(256);
    Key key = keygen.generateKey();
    // CBC
    String text = "secret!!secret!!secret!!secret!!";
    String transformation = "AES/CBC/PKCS5Padding";
    Cipher cipher = Cipher.getInstance(transformation);
    cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
    byte[] enc = cipher.doFinal(text.getBytes());
    System.out.println(DatatypeConverter.printHexBinary(enc));
    // Decrypt
    cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
    String result = new String(cipher.doFinal(enc));
    System.out.println(result);
  }
}
正如我们所看到的,因此我们没有看到重复的密码块。因此不推荐使用 ECB 模式,因为 使得看到重复并使用这些知识进行解密成为可能。有关ECB和CBC的更多信息,我建议您阅读资料:“电子密码本模式”。但对称加密有一个明显的问题——你需要以某种方式将密钥从加密者转移到加密者。而沿着这个路径,这个key就可以被截获,那么就有可能截获数据。而非对称加密就是为了解决这个问题而设计的。
Java 密码学架构:初次介绍 - 10

非对称加密

非对称加密或公钥加密是一种使用一对密钥的加密方法:私钥(对所有人保密)和公钥(公众可用)。为了在信息交换各方之间安全地交换公钥,同时保证秘密密钥的安全,这种分离是必要的。创建密钥对时,KeyGenerator 对我们来说已经不够了;我们需要KeyPairGenerator。让我们看一个例子:
import javax.crypto.*;
import java.security.*;
public class Main {
  public static void main(String[] args) throws Exception {
    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
    generator.initialize(1024);
    KeyPair keyPair = generator.generateKeyPair();
    // Encrypt with PRIVATE KEY
    Cipher cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
    byte[] data = cipher.doFinal("Hello!".getBytes());
    // Decrypt with PUBLIC KEY
    cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
    byte[] result = cipher.doFinal(data);
    System.out.println(new String(result));
  }
}
这里需要理解的是,在使用非对称加密时,我们总是使用KeyPair来使用一个密钥进行加密,另一个密钥进行解密。但是因为 加密的要点是只有接收者才能解密;它用公钥加密,只能用私钥解密。
Java 密码学架构:初识 - 11

电子签名

正如我们在上面看到的,知道公钥,您可以发送数据,以便只有私钥的所有者才能解密它。也就是说,非对称加密的本质是任何人都加密,但只有我们读取。还有一个相反的过程 - 数字签名,由Signature类表示。数字签名可以使用以下算法:“签名算法”。JCA 文档建议仔细研究这两个:DSAwithMD5 和 RSAwithMD5 比 DSA 或 RSA 更好的是什么以及它们之间的区别是什么,您可以在此处阅读:“哪种最适合加密文件传输 - RSA 或 DSA? ”。或者阅读此处的讨论:“ SSH 身份验证密钥的 RSA 与 DSA ”。所以,数字签名。和以前一样,我们需要一个 KeyPair 和一个新的 Signature 类。如果您到目前为止已经在在线编译器中进行了测试,那么下面的示例对他们来说可能有些困难。我的示例仅在这里运行:rextester.com。我们导入我们需要的类:
import javax.crypto.*;
import java.security.*;
我们还将重写 main 方法:
public static void main(String[] args) throws Exception {
    // Generate keys
    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
    SecureRandom random = SecureRandom.getInstanceStrong();
    generator.initialize(2048, random);
    KeyPair keyPair = generator.generateKeyPair();
    // Digital Signature
    Signature dsa = Signature.getInstance("SHA256withRSA");
    dsa.initSign(keyPair.getPrivate());
    // Update and sign the data
    Cipher cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
    byte[] data = cipher.doFinal("Hello!".getBytes());
    dsa.update(data);
    byte[] signature = dsa.sign();
    // Verify signature
    dsa.initVerify(keyPair.getPublic());
    dsa.update(data);
    boolean verifies = dsa.verify(signature);
    System.out.println("Signature is ok: " + verifies);
    // Decrypt if signature is correct
    if (verifies) {
      cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
      byte[] result = cipher.doFinal(data);
      System.out.println(new String(result));
    }
}
这就是数字签名的工作原理。数字签名是一个有趣的话题。我建议您查看有关此主题的报告:
Java 密码学架构:初识 - 12
上面我们看到了各方如何交换数据。JCA 中是否没有提供一些用于此交互的标准接口?事实证明是有的。我们来看一下。
Java 密码学架构:初识 - 13

关键协议

Java密码体系结构引入了一个重要的工具——密钥协商协议。它由KeyAgreement类表示。正如 JCA 文档中所述,该协议允许多方设置相同的加密密钥,而无需在各方之间共享任何秘密信息。听起来怪怪的?然后我们看一个例子:
// 1. Одна из сторон (Алиса) генерирует пару ключей. Encoded публичный ключ отдаёт.
KeyPairGenerator generator = KeyPairGenerator.getInstance("DH");
KeyPair aliceKeyPair = generator.generateKeyPair();
byte[] alicePubKeyEncoded = aliceKeyPair.getPublic().getEncoded();

// 2. Другая сторона (например, Боб) получает открытый ключ Алисы
KeyFactory bobKeyFactory = KeyFactory.getInstance("DH");
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(alicePubKeyEncoded);
PublicKey alicePubKey = bobKeyFactory.generatePublic(x509KeySpec);
// Параметры, которые использовала Алиса при генерации ключей
DHParameterSpec dhParamFromAlicePubKey = ((DHPublicKey)alicePubKey).getParams();
// Создаёт свою пару ключей. Отдаёт свой Encoded открытый ключ
KeyPairGenerator bobKpairGen = KeyPairGenerator.getInstance("DH");
bobKpairGen.initialize(dhParamFromAlicePubKey);
KeyPair bobKeyPair = bobKpairGen.generateKeyPair();
byte[] bobPubKeyEncoded = bobKeyPair.getPublic().getEncoded();

Теперь, у Алисы есть открытый ключ Боба, а у Боба есть открытый ключ Алисы. What дальше?
Как сказано в documentации JCA, у нас есть инструмент KeyAgreement, https://docs.oracle.com/javase/8/docs/technotes/guides/security/crypto/CryptoSpec.html#KeyAgreement который позволяет установить одинаковые ключи шифрования без необходимости обмениваться секретной информацией (т.е. без обмена private key). Соглашение выглядит следующим образом:
// 3. Соглашение по протоколу Диффи-Хеллмана (Diffie–Hellman, DH)
KeyAgreement aliceKeyAgree = KeyAgreement.getInstance("DH");
aliceKeyAgree.init(aliceKeyPair.getPrivate());
// Алиса на основе ключа боба и своего private key создаёт общий shared ключ
KeyFactory aliceKeyFactory = KeyFactory.getInstance("DH");
x509KeySpec = new X509EncodedKeySpec(bobPubKeyEncoded);
PublicKey bobPubKey = aliceKeyFactory.generatePublic(x509KeySpec);
aliceKeyAgree.doPhase(bobPubKey, true);
byte[] aliceSharedSecret = aliceKeyAgree.generateSecret();
SecretKeySpec aliceAesKey = new SecretKeySpec(aliceSharedSecret, 0, 16, "AES");
// Боб на основе ключа Алисы и своего private key создаёт общий shared ключ
KeyAgreement bobKeyAgree = KeyAgreement.getInstance("DH");
bobKeyAgree.init(bobKeyPair.getPrivate());
bobKeyAgree.doPhase(alicePubKey, true);
byte[] bobSharedSecret = bobKeyAgree.generateSecret();
SecretKeySpec bobAesKey = new SecretKeySpec(bobSharedSecret, 0, 16, "AES");
// Общий ключ у Алисы и Боба одинаков
System.out.println("Shared keys are equals: " + Arrays.equals(aliceSharedSecret, bobSharedSecret));

Далее Боб и Алиса, используя общий ключ, про который больше никто не знает, обмениваются зашифрованными данными:
// 4. Боб шифрует сообщение для Алисы
Cipher bobCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
bobCipher.init(Cipher.ENCRYPT_MODE, bobAesKey);
byte[] ciphertext = bobCipher.doFinal("Hello, Alice!".getBytes());
// Передаёт Алисе параметры, с которыми выполнялась шифровка
byte[] encodedParamsFromBob = bobCipher.getParameters().getEncoded();

// 5. Алиса принимает сообщение и расшифровывает его
AlgorithmParameters aesParams = AlgorithmParameters.getInstance("AES");
aesParams.init(encodedParamsFromBob);
Cipher aliceCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
aliceCipher.init(Cipher.DECRYPT_MODE, aliceAesKey, aesParams);
byte[] recovered = aliceCipher.doFinal(ciphertext);
System.out.println(new String(recovered));
此示例取自 JCA 文档示例:“ 2方之间的 Diffie-Hellman 密钥交换”。这大致就是使用密钥协商协议的 Java 加密体系结构中的非对称加密的样子。有关非对称加密的更多信息,推荐视频:
Java 密码学架构:初识 - 14

证书

好吧,对于甜点,我们还有同样重要的东西——证书。通常,证书是使用 jdk 附带的 keytool 实用程序生成的。您可以阅读更多详细信息,例如,此处:“使用 Java keytool 命令生成自签名 SSL 证书”。您还可以阅读 Oracle 的手册。例如,此处:“使用 keytool 创建服务器证书”。例如,让我们使用Tutorialspoint Java Online Compiler
import sun.security.tools.keytool.CertAndKeyGen;
import sun.security.x509.*;
import java.security.cert.*;
import java.security.*;
// Compiler args: -XDignore.symbol.file
public class Main {
  public static void main(String[] args) throws Exception {
    CertAndKeyGen certGen = new CertAndKeyGen("RSA", "SHA256WithRSA", null);
    // generate it with 2048 bits
    certGen.generate(2048);
    PrivateKey privateKey = certGen.getPrivateKey();
    X509Key publicKey = certGen.getPublicKey();
    // prepare the validity of the certificate
    long validSecs = (long) 365 * 24 * 60 * 60; // valid for one year
    // enter your details according to your application
    X500Name principal = new X500Name("CN=My Application,O=My Organisation,L=My City,C=DE");
    // add the certificate information, currently only valid for one year.
    X509Certificate cert = certGen.getSelfCertificate(principal, validSecs);
    // Public Key from Cert equals Public Key from generator
    PublicKey publicKeyFromCert = cert.getPublicKey();
    System.out.println(publicKeyFromCert.equals(publicKey));
  }
}
正如我们所看到的,证书提供了提供公钥的能力。这种方法有一个缺点 - 我们使用sun.security,这被认为是有风险的,因为...... 该包不是公共 Java API 的一部分。这就是为什么在编译期间需要指定参数 - XDignore.symbol.file。还有另一种方法 - 手动创建证书。缺点是它使用了未记录的内部 API。然而,了解它是很有用的。至少,因为 RFC-2459 规范的使用方式清晰可见:“ Internet X.509 公钥基础设施”。这是一个例子:
// 1. Генерируем пару ключей
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(4096);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 2. Определяем данные сертификата
// Определяем срок действия сертификата
Date from = new Date();
Date to = new Date(from.getTime() + 365 * 1000L * 24L * 60L * 60L);
CertificateValidity interval = new CertificateValidity(from, to);
// Определяем subject name, т.е. Name того, с чем ассоциирован публичный ключ
// CN = Common Name. Через точку с запятой могут быть указаны также другие атрибуты
// См. https://docs.oracle.com/cd/E24191_01/common/tutorials/authz_cert_attributes.html
X500Name owner = new X500Name("cn=Unknown");
// Уникальный в пределах CA, т.е. Certificate Authority (тот, кто выдаёт сертификат) номер
BigInteger number = new BigInteger(64, new SecureRandom());
CertificateSerialNumber serialNumber = new CertificateSerialNumber(number);
// Определяем алгоритм подписи сертификата
AlgorithmId algorithmId = new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid);
CertificateAlgorithmId certificateAlgorithmId = new CertificateAlgorithmId(algorithmId);
// 3. По подготовленной информации создаём сертификат
X509CertInfo info = new X509CertInfo();
info.set(X509CertInfo.VALIDITY, interval);
info.set(X509CertInfo.SERIAL_NUMBER, serialNumber);
info.set(X509CertInfo.SUBJECT, owner);
info.set(X509CertInfo.ISSUER, owner);
info.set(X509CertInfo.KEY, new CertificateX509Key(keyPair.getPublic()));
info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
info.set(X509CertInfo.ALGORITHM_ID, certificateAlgorithmId);
// 4. Подписываем сертификат
X509CertImpl certificate = new X509CertImpl(info);
certificate.sign(keyPair.getPrivate(), "SHA256withRSA");
// 5. Проверка сертификата
try {
	// В случае ошибки здесь будет брошено исключение. Например: java.security.SignatureException
	certificate.verify(keyPair.getPublic());
} catch (Exception e) {
	throw new IllegalStateException(e);
}
Java 密码学架构:初次介绍 - 15

密钥库(KeyStore)

最后我要讲的是密钥和证书存储,称为KeyStore。显然,不断生成证书和密钥是昂贵且毫无意义的。因此,它们需要以某种方式安全地存储。有一个工具可以用于此目的 - KeyStore。密钥存储在 JCA 文档的“密钥管理”一章中进行了描述。使用它的 API 非常清晰。这是一个小例子:
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
String alias = "EntityAlias";
java.security.cert.Certificate[] chain = {certificate};
keyStore.setKeyEntry(alias, keyPair.getPrivate(), "keyPassword".toCharArray(), chain);
// Загрузка содержимого (Private Key + Certificate)
Key key = keyStore.getKey(alias, "keyPassword".toCharArray());
Certificate[] certificateChain = keyStore.getCertificateChain(alias);
// Сохранение KeyStore на диск
File file = File.createTempFile("security_", ".ks");
System.out.println(file.getAbsolutePath());
try (FileOutputStream fos = new FileOutputStream(file)) {
	keyStore.store(fos, "keyStorePassword".toCharArray());
}
从示例中可以看到,它首先load针对 KeyStore 执行。但在我们的例子中,我们将第一个属性指定为 null,即 没有 KeyStore 的来源。这意味着 KeyStore 被创建为空,以便进一步保存。第二个参数也为空,因为 我们正在创建一个新的密钥库。如果我们从文件加载 KeyStore,那么我们需要在此处指定一个密码(类似于名为 store 的 KeyStore 方法)。

底线

因此,我们与您一起回顾了 Java 加密体系结构(又名 JCA)框架内最基本和最基本的操作。我们了解了什么是对称加密和非对称加密以及它是如何在 JCA 中实现的。我们了解了如何创建证书和数字签名以及如何使用它们。这些都只是基础,背后还有很多更复杂、更有趣的东西。我希望这篇综述材料对您有用,并使您对进一步研究该领域感兴趣。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION