数据交换的安全性是现代应用程序最重要的属性之一。自古以来,人们就想出了巧妙的方法,随着人类的发展,密码学成为一门完整的科学。当然,Java 并没有袖手旁观,而是为开发人员提供了 Java 密码体系结构 (JCA)。这篇评论应该让您初步了解它是如何工作的。
这可能意味着什么:
正如文档所述,“ Java 平台包含许多内置提供程序”。也就是说,Java 平台提供了一组内置的提供程序,可以根据需要进行扩展。你可以自己看看:
代码中可能如下所示:
为了避免在这种情况下重播,您应该使用另一种模式 - 密码块链接 (CBC)。该模式引入了初始化向量的概念(由 IvParameterSpec 类表示)。也得益于这种模式,生成最后一个块的结果将用于生成下一个块:
现在让我们将其写在代码中:
上面我们看到了各方如何交换数据。JCA 中是否没有提供一些用于此交互的标准接口?事实证明是有的。我们来看一下。
前言
我建议回到过去。摆在我们面前的是古罗马。在我们面前的是盖乌斯·朱利叶斯·凯撒,他向他的指挥官们发送了一条信息。我们来看看这条消息的内容:"ЕСКЕУГЬГМХИФЯ Е УЛП"
?我们打开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 获得了哪些机会。
J.C.A.
正如我们之前了解到的,Java 提供了 Java 密码体系结构 (JCA) 来处理密码学。该架构包含一个 API(即一组特定的接口)和提供者(实现它们):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 ”)。
信息摘要
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);
但哈希是一种单向函数。但是如果您希望能够加密和解密怎么办?
对称密钥密码学
对称加密是使用相同密钥进行加密和解密的加密。为了使用对称加密,我们需要一个密钥。为了获得它,我们使用KeyGenerator。此外,我们还需要一个表示密码(Cipher)的类。正如 JCA 文档中“创建密码对象”部分所述,要创建密码,您不仅需要指定算法,还需要在行中指定“转换”。转换描述如下所示:“算法/模式/填充”:- 算法:这里我们看一下“密码(加密)算法”的标准名称。推荐使用AES。
- 模式:加密模式。例如:ECB 或 CBC(我们稍后会讨论这个)
- 缩进/分割:每个数据块都单独加密。该参数决定多少数据算作 1 个块。
"AES/ECB/PKCS5Padding"
。即加密算法为AES,加密方式为ECB(Electronic Codebook的缩写),块大小为PKCS5Padding。PKCS5Padding 表示一个块的大小为 2 字节(16 位)。电子密码本加密模式涉及每个块的顺序加密:
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 位块:
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就可以被截获,那么就有可能截获数据。而非对称加密就是为了解决这个问题而设计的。
非对称加密
非对称加密或公钥加密是一种使用一对密钥的加密方法:私钥(对所有人保密)和公钥(公众可用)。为了在信息交换各方之间安全地交换公钥,同时保证秘密密钥的安全,这种分离是必要的。创建密钥对时,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来使用一个密钥进行加密,另一个密钥进行解密。但是因为 加密的要点是只有接收者才能解密;它用公钥加密,只能用私钥解密。
电子签名
正如我们在上面看到的,知道公钥,您可以发送数据,以便只有私钥的所有者才能解密它。也就是说,非对称加密的本质是任何人都加密,但只有我们读取。还有一个相反的过程 - 数字签名,由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密码体系结构引入了一个重要的工具——密钥协商协议。它由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 加密体系结构中的非对称加密的样子。有关非对称加密的更多信息,推荐视频:
证书
好吧,对于甜点,我们还有同样重要的东西——证书。通常,证书是使用 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);
}
密钥库(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 方法)。
GO TO FULL VERSION