许多人认为加密是一个复杂的主题,这很难理解。 虽然可以实现它的某些方面,但是每个人都可以理解它在更高层次上的工作方式。
这就是我要处理的这篇文章。 用简单的术语解释它是如何工作的,然后使用一些代码。
是的,我们信任加密。 信任是什么意思? 我们相信,我们的消息仅由授权方读取(机密性),在传输过程中不会更改(完整性),并且确实由我们认为已发送(验证)的人发送。
维基百科为加密提供了一个很好的定义:“是以一种仅授权方可以访问的方式对消息或信息进行编码的过程”。
因此,加密通过使用密钥(密码)将我们的信息转换为难以理解的密钥(密文),该密钥只能从授权方转回原始信息。
加密方案有两种类型, 对称和非对称密钥加密。
在对称加密中,相同的密钥用于加密和解密消息。 我们希望访问该消息的用户必须具有密钥,但没有其他密钥,否则我们的消息将受到威胁。
我对非对称密钥加密很感兴趣。 非对称密钥方案使用两个密钥:私有密钥和公共密钥。 这些键对很特殊。 它们之所以特别是因为它们是使用称为非对称算法的一类算法生成的。 实际的算法不在此讨论范围之内,但是在本教程的后面,我们将使用RSA。
您现在需要知道的是,这些键具有以下属性。 使用以下内容加密的消息:
- 只能使用私钥解密公钥
- 私钥只能使用公钥解密
看起来足够简单吧? 那么它在实践中如何使用? 让我们考虑两个朋友,爱丽丝和鲍勃。 他们有自己的一对公钥和私钥,并且希望在聊天中保持私密性。 他们每个人都公开提供他们的公钥,但是要小心地隐藏他们的私钥。
当爱丽丝(Alice)要发送仅从鲍勃(Bob)读取的消息时,她使用鲍勃(Bob)的公共密钥对消息进行加密。 然后Bob和只有他才能使用他的私钥解密消息。 而已。
这就解释了第一个属性的使用,但是第二个呢? 似乎没有理由使用我们的私钥进行加密。 好吧,有。 我们怎么知道爱丽丝是那个发信息的人? 如果我们可以使用Alice的公钥解密消息,则可以确保使用Alice的私钥进行加密,因此确实是从Alice发送的。 简单的说:
使用公钥,以便人们只能将内容发送给您,而私钥则用于证明您的身份。
因此,我们可以使用公钥来确保机密性 ,并使用私钥来保证真实性 。 那么诚信呢? 为了实现这一点,我们使用加密哈希。 良好的加密散列可接收输入消息并生成具有以下属性的消息摘要:
- 消息摘要易于生成
- 计算哪个输入提供了哈希值非常困难
- 两个不同的输入/消息生成相同的哈希值的可能性极小
如果我们想确保接收到的消息在过渡期间没有受到破坏,则将哈希值与加密消息一起发送。 在接收端,我们使用相同的算法对解密后的消息进行哈希处理,然后进行比较以确保哈希值完全匹配。 如果它们是,那么我们可以确信消息没有被更改。
这些散列或消息摘要也具有其他用途。 您会看到,有时Bob做出承诺,然后否认自己曾经做过。 我们想让他检查。 用奇特的术语来说,这是不可否认的 ,它可以防止各方拒绝发送消息。 数字签名是众所周知的应用。
在继续学习并享受代码乐趣之前,让我再说几件事。
- 非对称密钥算法实际上具有两种用于不同功能的算法。 一个当然是用于密钥生成,另一个功能是用于功能评估。 功能评估意味着获取输入(即消息)和密钥,并根据获得的输入结果得到加密或解密的消息。 因此功能评估就是使用公钥/私钥对消息进行加密和解密的方式。
- 也许您已经想过,我们怎么知道公钥实际上与Bob或Alice相关? 如果有人假装自己怎么办? 有一个标准可以帮助我们。 X.509定义了公钥证书的格式。 这些证书由证书颁发机构提供,通常包含:
- 主题,聚会的详细说明(例如Alice)
- 有效期,证书有效期
- 公钥,可帮助我们向聚会发送加密的消息
- 证书颁发机构,证书的颁发者
- 哈希和加密是不同的东西。 加密的消息最终将被转换回原始消息。 散列的消息应该不可能转回原始消息。
现在,让我们使用一个教程来帮助所有这些问题。我们将允许三个人爱丽丝,鲍勃和保罗与机密性,完整性和身份验证进行通信(进一步将它们称为CIA)。 完整的代码可在github上找到 。
该项目有几个依赖项,如下所示:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.tasosmartidis.tutorial.encryption</groupId><artifactId>encryption-tutorial</artifactId><version>1.0-SNAPSHOT</version><packaging>jar</packaging><name>encryption-tutorial</name><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><lombok.version>1.16.18</lombok.version><commons-codec.version>1.11</commons-codec.version><junit.version>4.12</junit.version><bouncycastle.version>1.58</bouncycastle.version></properties><dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></dependency><dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId><version>${commons-codec.version}</version></dependency><dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15on</artifactId><version>${bouncycastle.version}</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>${junit.version}</version><scope>test</scope></dependency></dependencies><build><finalName>encryption-tutorial</finalName><pluginManagement><plugins><plugin><artifactId>maven-compiler-plugin</artifactId><version>3.0</version><configuration><source>1.8</source><target>1.8</target></configuration></plugin></plugins></pluginManagement></build>
</project>
我们将从EncryptedMessage类开始,该类将提供我们确保CIA所需的所有信息。 该消息将包含用于机密性的实际加密消息,该消息的哈希值将用于确保发送者的完整性和身份验证,原始消息和加密消息则用于身份验证。 我们还提供了一种破坏消息有效负载的方法,因此我们可以针对摘要测试验证(稍后会进行介绍)。
package com.tasosmartidis.tutorial.encryption.domain;import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;@AllArgsConstructor
@Getter
@EqualsAndHashCode
public class EncryptedMessage {private String encryptedMessagePayload;private String senderId;private String encryptedSenderId;private String messageDigest;// FOR DEMO PURPOSES ONLY!public void compromiseEncryptedMessagePayload(String message) {this.encryptedMessagePayload = message;}@Overridepublic String toString() {return encryptedMessagePayload;}
}
现在让我们进入加密部分。 我们将创建一个独立于实际非对称算法和密钥长度的基本加密器类。 它将创建密钥和密码,具有用于加密和解密文本以及提供对密钥的访问的方法。 看起来像这样:
package com.tasosmartidis.tutorial.encryption.encryptor;import com.tasosmartidis.tutorial.encryption.domain.EncryptorProperties;
import com.tasosmartidis.tutorial.encryption.exception.DecryptionException;
import com.tasosmartidis.tutorial.encryption.exception.EncryptionException;
import com.tasosmartidis.tutorial.encryption.exception.EncryptorInitializationException;
import com.tasosmartidis.tutorial.encryption.exception.UnauthorizedForDecryptionException;
import org.apache.commons.codec.binary.Base64;import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.nio.charset.StandardCharsets;
import java.security.*;public class BaseAsymmetricEncryptor {private final KeyPairGenerator keyPairGenerator;private final KeyPair keyPair;private final Cipher cipher;private final EncryptorProperties encryptorProperties;protected BaseAsymmetricEncryptor(EncryptorProperties encryptorProperties) {this.encryptorProperties = encryptorProperties;this.keyPairGenerator = generateKeyPair();this.keyPairGenerator.initialize(this.encryptorProperties.getKeyLength());this.keyPair = this.keyPairGenerator.generateKeyPair();this.cipher = createCipher(encryptorProperties);}protected PrivateKey getPrivateKey() {return this.keyPair.getPrivate();}public PublicKey getPublicKey() {return this.keyPair.getPublic();}protected String encryptText(String textToEncrypt, Key key) {try {this.cipher.init(Cipher.ENCRYPT_MODE, key);return Base64.encodeBase64String(cipher.doFinal(textToEncrypt.getBytes(StandardCharsets.UTF_8)));} catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException ex) {throw new EncryptionException("Encryption of message failed", ex);}}protected String decryptText(String textToDecrypt, Key key) {try {this.cipher.init(Cipher.DECRYPT_MODE, key);return new String(cipher.doFinal(Base64.decodeBase64(textToDecrypt)), StandardCharsets.UTF_8);}catch (InvalidKeyException | BadPaddingException ex){throw new UnauthorizedForDecryptionException("Not authorized to decrypt message", ex);} catch (IllegalBlockSizeException ex) {throw new DecryptionException("Decryption of message failed", ex);}}private Cipher createCipher(EncryptorProperties encryptorProperties) {try {return Cipher.getInstance(encryptorProperties.getAsymmetricAlgorithm());} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {throw new EncryptorInitializationException("Creation of cipher failed", ex);}}private KeyPairGenerator generateKeyPair() {try {return KeyPairGenerator.getInstance(this.encryptorProperties.getAsymmetricAlgorithm());} catch (NoSuchAlgorithmException ex) {throw new EncryptorInitializationException("Creation of encryption keypair failed", ex);}}}
为了实现我们的功能,我们需要处理许多异常,但是由于万一发生它们,我们将不对其进行任何处理,因此我们将它们包装在语义上有意义的运行时异常中。 我不会在这里展示异常类,因为它们只有一个构造函数。 但是您可以在github.com.tasosmartidis.tutorial.encryption.exception包下的项目中检出它们。
您将在代码的不同部分看到它们的实际用法。 BaseAsymmetricEncryptor
的构造BaseAsymmetricEncryptor
将EncryptorProperites
实例作为参数。
package com.tasosmartidis.tutorial.encryption.domain;import lombok.AllArgsConstructor;@AllArgsConstructor
public class EncryptorProperties {private final AsymmetricAlgorithm asymmetricAlgorithm;private final int keyLength;public String getAsymmetricAlgorithm() {return asymmetricAlgorithm.toString();}public int getKeyLength() {return keyLength;}
}
我们将创建一个基于RSA的加密器实现。 该代码应该说明一切:
package com.tasosmartidis.tutorial.encryption.encryptor;import com.tasosmartidis.tutorial.encryption.domain.AsymmetricAlgorithm;
import com.tasosmartidis.tutorial.encryption.domain.EncryptedMessage;
import com.tasosmartidis.tutorial.encryption.domain.EncryptorProperties;
import org.bouncycastle.jcajce.provider.digest.SHA3;
import org.bouncycastle.util.encoders.Hex;import java.security.PublicKey;public class RsaEncryptor extends BaseAsymmetricEncryptor {private static final int KEY_LENGTH = 2048;public RsaEncryptor() {super(new EncryptorProperties(AsymmetricAlgorithm.RSA, KEY_LENGTH));}public String encryptMessageForPublicKeyOwner(String message, PublicKey key) {return super.encryptText(message, key);}public String encryptMessageWithPrivateKey(String message) {return super.encryptText(message, super.getPrivateKey());}public String decryptReceivedMessage(EncryptedMessage message) {return super.decryptText(message.getEncryptedMessagePayload(), super.getPrivateKey());}public String decryptMessageFromOwnerOfPublicKey(String message, PublicKey publicKey) {return super.decryptText(message, publicKey);}public String hashMessage(String message) {SHA3.DigestSHA3 digestSHA3 = new SHA3.Digest512();byte[] messageDigest = digestSHA3.digest(message.getBytes());return Hex.toHexString(messageDigest);}
}
对于我们的演示,我们将需要演员,彼此交流信息的人。 每个人都将具有唯一的身份,姓名以及与之通信的受信任联系人的列表。
package com.tasosmartidis.tutorial.encryption.demo;import com.tasosmartidis.tutorial.encryption.domain.EncryptedMessage;
import com.tasosmartidis.tutorial.encryption.message.RsaMessenger;
import lombok.EqualsAndHashCode;import java.security.PublicKey;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;@EqualsAndHashCode
public class Person {private final String id;private final String name;private final Set<Person> trustedContacts;private final RsaMessenger rsaMessenger;public Person(String name) {this.id = UUID.randomUUID().toString();this.name = name;this.trustedContacts = new HashSet<>();this.rsaMessenger = new RsaMessenger(this.trustedContacts, this.id);}public PublicKey getPublicKey() {return this.rsaMessenger.getPublicKey();}public String getName() {return name;}public String getId() {return id;}public void addTrustedContact(Person newContact) {if(trustedContacts.contains(newContact)) {return;}trustedContacts.add(newContact);}public EncryptedMessage sendEncryptedMessageToPerson(String message, Person person) {return this.rsaMessenger.encryptMessageForPerson(message, person);}public void readEncryptedMessage(EncryptedMessage encryptedMessage) {this.rsaMessenger.readEncryptedMessage(encryptedMessage);}}
接下来,让我们创建一个RsaMessanger
类,该类将允许人们使用RsaEncryptor
发送加密的消息。 发送加密的消息时,我们将提供所有必要的信息,以确保机密性,完整性和身份验证。 阅读时,我们将解密消息,我们将尝试验证消息是由受信任的联系人发送的,并确保消息没有受到破坏或更改。
package com.tasosmartidis.tutorial.encryption.message;import com.tasosmartidis.tutorial.encryption.demo.Person;
import com.tasosmartidis.tutorial.encryption.domain.EncryptedMessage;
import com.tasosmartidis.tutorial.encryption.encryptor.RsaEncryptor;
import com.tasosmartidis.tutorial.encryption.exception.PayloadAndDigestMismatchException;import java.security.PublicKey;
import java.util.Optional;
import java.util.Set;public class RsaMessenger {private final RsaEncryptor encryptionHandler;private final Set<Person> trustedContacts;private final String personId;public RsaMessenger(Set<Person> trustedContacts, String personId) {this.encryptionHandler = new RsaEncryptor();this.trustedContacts = trustedContacts;this.personId = personId;}public PublicKey getPublicKey() {return this.encryptionHandler.getPublicKey();}public EncryptedMessage encryptMessageForPerson(String message, Person person) {String encryptedMessage = this.encryptionHandler.encryptMessageForPublicKeyOwner(message, person.getPublicKey());String myEncryptedId = this.encryptionHandler.encryptMessageWithPrivateKey(this.personId);String hashedMessage = this.encryptionHandler.hashMessage(message);return new EncryptedMessage(encryptedMessage, this.personId, myEncryptedId, hashedMessage);}public void readEncryptedMessage(EncryptedMessage message) {String decryptedMessage = this.encryptionHandler.decryptReceivedMessage(message);Optional<Person> sender = tryIdentifyMessageSender(message.getSenderId());if(!decryptedMessageHashIsValid(decryptedMessage, message.getMessageDigest())) {throw new PayloadAndDigestMismatchException("Message digest sent does not match the one generated from the received message");}if(sender.isPresent() && senderSignatureIsValid(sender.get(), message.getEncryptedSenderId())) {System.out.println(sender.get().getName() +" send message: " + decryptedMessage);}else {System.out.println("Unknown source send message: " + decryptedMessage);}}private boolean senderSignatureIsValid(Person sender, String encryptedSenderId) {if(rawSenderIdMatchesDecryptedSenderId(sender, encryptedSenderId)) {return true;}return false;}private boolean rawSenderIdMatchesDecryptedSenderId(Person sender, String encryptedSenderId) {return sender.getId().equals(this.encryptionHandler.decryptMessageFromOwnerOfPublicKey(encryptedSenderId, sender.getPublicKey()));}private Optional<Person> tryIdentifyMessageSender(String id) {return this.trustedContacts.stream().filter(contact -> contact.getId().equals(id)).findFirst();}private boolean decryptedMessageHashIsValid(String decryptedMessage, String hashedMessage) {String decryptedMessageHashed = this.encryptionHandler.hashMessage(decryptedMessage);if(decryptedMessageHashed.equals(hashedMessage)) {return true;}return false;}
}
好的! 现在是演示时间!
我们将创建一些测试以确保一切正常。 我们要测试的方案是:
当爱丽丝(鲍勃的受信任联系人)向他发送加密的消息时,鲍勃可以解密该消息,并且知道来自鲍勃。 还要确保有效载荷没有被更改。
从Alice到Bob的相同消息不可用于Paul解密,并且将引发UnauthorizedForDecryptionException
。 当保罗(鲍勃不知道)发送加密消息时,鲍勃将能够读取该消息,但不知道是谁发送了该消息。 最后,当我们危及加密消息的有效负载时,带有消息摘要的验证将识别出该消息并引发异常。
package com.tasosmartidis.tutorial.encryption;import com.tasosmartidis.tutorial.encryption.demo.Person;
import com.tasosmartidis.tutorial.encryption.domain.EncryptedMessage;
import com.tasosmartidis.tutorial.encryption.exception.PayloadAndDigestMismatchException;
import com.tasosmartidis.tutorial.encryption.exception.UnauthorizedForDecryptionException;
import org.junit.Before;
import org.junit.Test;public class DemoTest {private static final String ALICE_MESSAGE_TO_BOB = "Hello Bob";private static final String PAULS_MESSAGE_TO_BOB = "Hey there Bob";private final Person bob = new Person("Bob");private final Person alice = new Person("Alice");private final Person paul = new Person("Paul");private EncryptedMessage alicesEncryptedMessageToBob;private EncryptedMessage paulsEncryptedMessageToBob;@Beforepublic void setup() {bob.addTrustedContact(alice);alicesEncryptedMessageToBob = alice.sendEncryptedMessageToPerson(ALICE_MESSAGE_TO_BOB, bob);paulsEncryptedMessageToBob = paul.sendEncryptedMessageToPerson(PAULS_MESSAGE_TO_BOB, bob);}@Testpublic void testBobCanReadAlicesMessage() {bob.readEncryptedMessage(alicesEncryptedMessageToBob);}@Test(expected = UnauthorizedForDecryptionException.class)public void testPaulCannotReadAlicesMessageToBob() {paul.readEncryptedMessage(alicesEncryptedMessageToBob);}@Testpublic void testBobCanReadPaulsMessage() {bob.readEncryptedMessage(paulsEncryptedMessageToBob);}@Test(expected = PayloadAndDigestMismatchException.class)public void testChangedMessageIdentifiedAndRejected() {EncryptedMessage slightlyDifferentMessage = alice.sendEncryptedMessageToPerson(ALICE_MESSAGE_TO_BOB + " ", bob);alicesEncryptedMessageToBob.compromiseEncryptedMessagePayload(slightlyDifferentMessage.getEncryptedMessagePayload());bob.readEncryptedMessage(alicesEncryptedMessageToBob);}
}
运行测试将产生以下结果:
就是这样! 再次感谢您的阅读,您可以在github上找到代码。
翻译自: https://www.javacodegeeks.com/2017/11/encryption-trust-tutorial.html