Статьи

Мы верим в шифрование!

Многие люди считают шифрование сложной темой, которую трудно понять. И определенные аспекты его реализации могут быть, но каждый может понять, как это работает на более высоком уровне.

Это то, что я хочу сделать с этой статьей. Объясните простыми словами, как это работает, а затем поиграйтесь с некоторым кодом.

Да, в шифровании мы верим. Что я имею в виду под доверием? Мы верим, что наши сообщения читаются только уполномоченными сторонами (конфиденциальность), они не изменяются во время передачи (целостность) и действительно отправляются теми, кого мы считаем, что они были отправлены (аутентификация).

Википедия дает хорошее определение для шифрования: «это процесс кодирования сообщения или информации таким образом, что только авторизованные стороны могут получить к нему доступ».

Таким образом, шифрование превращает наше сообщение с использованием ключа (шифра) в непонятный (шифротекст), который может быть возвращен к оригиналу только уполномоченными лицами.

Существует два типа схем шифрования: симметричное и асимметричное шифрование ключей.

В симметричном шифровании один и тот же ключ используется для шифрования и дешифрования сообщения. Те, кому мы хотим получить доступ к сообщению, должны иметь ключ, но не более, иначе наши сообщения будут скомпрометированы.

Асимметричный ключ шифрования мой интерес здесь. Схемы асимметричного ключа, используйте два ключа, частный и открытый. Эти пары ключей особенные. Они особенные, потому что они генерируются с использованием категории алгоритмов, называемых асимметричными алгоритмами . Реальные алгоритмы выходят за рамки этого обсуждения, но позже в этом уроке мы будем использовать RSA.

Теперь вам нужно знать, что эти ключи имеют следующие свойства. Сообщение, зашифрованное с помощью:

  1. открытый ключ может быть расшифрован только с использованием закрытого ключа
  2. закрытый ключ может быть расшифрован только с использованием открытого ключа


Кажется достаточно простым, верно? Так как это используется на практике? Давайте рассмотрим двух друзей, Алису и Боба. У них есть свои пары открытых и закрытых ключей, и они хотят уединения в своих чатах. Каждый из них открыто предоставляет свой открытый ключ, но тщательно скрывает свой закрытый ключ.

Когда Алиса хочет отправить сообщение только для чтения от Боба, она использует открытый ключ Боба для шифрования сообщения. Тогда Боб и только он может расшифровать сообщение, используя свой закрытый ключ. Вот и все.

Это объясняет использование первого свойства, но как насчет второго? Кажется, нет никаких оснований для шифрования с использованием нашего личного ключа. Ну, есть. Откуда мы знаем, что Алиса отправила сообщение? Если мы сможем расшифровать сообщение, используя открытый ключ Алисы, мы можем быть уверены, что для шифрования использовался закрытый ключ Алисы, поэтому он действительно был отправлен Алисой. Проще говоря:

Открытый ключ используется, чтобы люди могли отправлять вещи только вам, а закрытый ключ используется для подтверждения вашей личности.

Таким образом, мы можем сохранять конфиденциальность, используя открытый ключ, и подлинность, используя закрытый. Как насчет честности ? Для этого мы используем криптографическое хеширование. Хороший криптографический хеш принимает входное сообщение и генерирует дайджест сообщения со следующими свойствами:

  1. Дайджест сообщения легко генерировать
  2. Чрезвычайно сложно рассчитать, какой вход предоставил хэш
  3. Крайне маловероятно, чтобы два разных ввода / сообщения генерировали одно и то же значение хеша

Если мы хотим быть уверены, что полученное сообщение не было скомпрометировано во время перехода, значение хеша отправляется вместе с зашифрованным сообщением. На принимающей стороне мы хешируем дешифрованное сообщение с помощью того же алгоритма и сравниваем, чтобы убедиться, что хэши точно совпадают. Если они есть, то мы можем быть уверены, что сообщение не было изменено.

Эти хеши или дайджесты сообщений также имеют и другое применение. Видите ли, иногда Боб дает обещания, а затем отрицает, что он когда-либо делал. Мы хотим держать его под контролем. В причудливых терминах это называется безотказностью и лишает стороны возможности отказать в отправке сообщения. Хорошо известным применением этого являются цифровые подписи.

Прежде чем мы переместимся и повеселимся с кодом, позвольте мне упомянуть еще пару вещей.

  1. Алгоритмы с асимметричным ключом на самом деле имеют два алгоритма для разных функций. Один из них предназначен для генерации ключей, а другой — для оценки функций. Оценка функции означает получение ввода (т. Е. Сообщения) и ключа и в результате зашифрованное или дешифрованное сообщение, в зависимости от полученного ввода. Таким образом, оценка функции — это то, как сообщения шифруются и дешифруются с использованием открытого / закрытого ключей.
  2. Может быть, вы уже подумали, откуда мы знаем, что открытый ключ действительно связан с Бобом или Алисой? Что если это кто-то притворяется ими? Существует стандарт, который может помочь нам в этом. Это X.509, который определяет формат для сертификатов открытых ключей. Эти сертификаты предоставляются сертификационными органами и обычно содержат:
    1. Тема, подробное описание партии (например, Алиса)
    2. Диапазон действия, как долго действителен сертификат
    3. Открытый ключ, который помогает нам отправлять зашифрованные сообщения на вечеринку
    4. Центр сертификации, эмитент сертификата
  3. Хеширование и шифрование — разные вещи. Зашифрованное сообщение должно быть в конечном итоге возвращено к исходному сообщению. Хешированное сообщение не может быть возвращено к оригиналу.

Теперь давайте воспользуемся учебным пособием, чтобы помочь всем этим освоиться. Мы позволим трем людям Алисе, Бобу и Полу общаться с Конфиденциальностью, Целостностью и Аутентификацией (далее будем называть их ЦРУ). Полный код доступен на github .
У проекта есть пара зависимостей, как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
    <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. Сообщение будет содержать фактическое зашифрованное сообщение для конфиденциальности, хеш сообщения, который будет использоваться для обеспечения целостности и идентификации отправителя, необработанный и зашифрованный для аутентификации. Мы также предоставляем метод для компрометации полезной нагрузки сообщения, чтобы мы могли проверить проверку на соответствие дайджесту (подробнее об этом позже).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
    }
 
    @Override
    public String toString() {
        return encryptedMessagePayload;
    }
}

Теперь давайте перейдем к части шифрования. Мы создадим базовый класс шифрования, независимый от фактического асимметричного алгоритма и длины ключа. Он будет создавать ключи и шифровать, иметь методы для шифрования и дешифрования текста, а также предоставит доступ к ключам. Это выглядит примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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 принимает экземпляр EncryptorProperites в качестве аргумента.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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. Код должен говорить сам за себя:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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);
    }
}

Для нашей демонстрации нам понадобятся актеры, люди, которые будут обмениваться сообщениями друг с другом. У каждого человека будет уникальная личность, имя и список доверенных контактов, с которыми он общается.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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 . При отправке зашифрованного сообщения мы предоставим всю необходимую информацию, чтобы гарантировать конфиденциальность, целостность и аутентификацию. При чтении мы расшифруем сообщение, постараемся убедиться, что оно отправлено доверенным контактом, и убедиться, что сообщение не было скомпрометировано или изменено.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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;
    }
}

Хорошо! Это демо время!

Мы создадим несколько тестов, чтобы убедиться, что все работает как положено. Сценарии, которые мы хотим протестировать:
Когда Алиса (доверенный контакт Боба) отправляет ему зашифрованное сообщение, Боб может расшифровать его и узнать, что оно от Боба. Также, чтобы гарантировать, что полезная нагрузка не была изменена.
То же сообщение от Алисы Бобу не доступно для расшифровки Полом, и будет UnauthorizedForDecryptionException .
Когда Пол (неизвестный Бобу) отправляет зашифрованное сообщение, Боб сможет прочитать его, но не сможет узнать, кто его отправил.
Наконец, когда мы компрометируем полезную нагрузку зашифрованного сообщения, проверка с его дайджестом сообщения распознает его и выдаст исключение.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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;
 
    @Before
    public void setup() {
        bob.addTrustedContact(alice);
        alicesEncryptedMessageToBob = alice.sendEncryptedMessageToPerson(ALICE_MESSAGE_TO_BOB, bob);
        paulsEncryptedMessageToBob = paul.sendEncryptedMessageToPerson(PAULS_MESSAGE_TO_BOB, bob);
    }
 
    @Test
    public void testBobCanReadAlicesMessage() {
        bob.readEncryptedMessage(alicesEncryptedMessageToBob);
    }
 
    @Test(expected = UnauthorizedForDecryptionException.class)
    public void testPaulCannotReadAlicesMessageToBob() {
        paul.readEncryptedMessage(alicesEncryptedMessageToBob);
    }
 
    @Test
    public 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 .

Смотрите оригинальную статью здесь: Мы верим в шифрование! Учебник

Мнения, высказанные участниками Java Code Geeks, являются их собственными.