В данном руководстве мы рассмотрим работу с OpenPGP на Java с использованием библиотеки Bouncy Castle Cryptography Library с ориентацией на использование в веб-разработке.

image

(ссылка на источник схемы)

PGP/OpenPGP


OpenPGP описан в RFC 4880, и является наиболее распространенным стандартом криптографической системы с открытым ключем для работы с цифровыми подписями и шифрованием сообщений. Поддерживается множеством программ и библиотек, в том числе с свободных с отрытым кодом. В частности, следует отметить GnuPG, Kleopatra, набор утилит для Windows Gpg4win (в него входят GnuPG, Kleopatra и др.), расширение для Thunderbird Enigmail.
Для Windows и также следует обратить внимание на коммерческий пакет Symantec Endpoint Encryption, известный также как PGP Desktop (доступна пробная версия, которую после истечения срока можно продолжать использовать для некоммерческих целей), и другие продукты из Symantec Encryption Family.

Подробнее см.: Введение в шифрование с открытым ключом и PGP, Механизм работы PGP.

Bouncy Castle Cryptography Library


Bouncy Castle Cryptography Library в настоящее время является наиболее используемой криптографической библиотекой для Java, в том чиле используется в Android.
Это проект с открытым кодом (GitHub: github.com/bcgit/bc-java ). Также для нашей темы интерес представляет открытый код программы Portable PGP v. 1.x, написанной на Java c использованием Bouncy Castle: sourceforge.net/p/ppgp/code

Для работы с OpenPGP в Bouncy Castle есть отдельный пакет org.bouncycastle.openpgp, с которым мы и будем работать.

Считываем публичный ключ


Решаемая задача: считать ASCII-armored Public PGP Key Block (публичный ключ) в виде String и создать org.bouncycastle.openpgp.PGPPublicKey объект, представляющий публичный ключ в BC.

ASCII-armored формат представляет ключ текстовом формате, который удобно использовать для публикации на сайте, пересылки по электронной почте и т.д., выглядит так:
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v2.0.22 (GNU/Linux)

mQINBFZpf0wBEADo7NZ4Oymw2pVJMfrNEhGwYOT4CGMOTZHQo3rYFrN3yxCsd/xX
r8kWLvkKvEFSD0XfQlq6slA9fnOtsRZl4JlmabKC33ZkB1zhV2c78AhhVMrfFi2a
114xHhOHkR4LOv8mAyyRKd5mpuyYpPKcF2670jXxANeqNQCoKKM/dPS1uxGapE9Z
GG0GFKvIUqKWJUAv7JqOAxtXbAS7JFMwiH16jL/TeuvKy+0JKvlBM4qxB9S18hi4
GYg1SEATqmeu9E+6alz0L25REYYZZOMja1quF8HywsRY0fSZqCbD+9diP0keAg6z
PSDSlgDF4WBUi+c8PoXcRRZq7+XYgky4l8wfGoGnOZKA4GH2SMlNAX8jCIrC7Cpf
KPCu6NMY533X735Md6fnWOeuyxz4owPRb6OTt4rYiA3/9vCu/waZ1zZx/wATYORS
1wmOx8ZjogeAPOz6a2PW8hrK0tWbT3ILucC7dxjYfdKHZX2+v3eMGvXFRKss8J5i
E5rlJiDnAMoERmIJunh/EJHz4te+RdMy2jWeDQWGzaFyZC92SUo9tAUJk7xez0KX
5sYzhXjFJX/17DwFBTXIOYpFjWUyPaQaP7ByWOjZzhCmQxYRBUJSoxOty1ngkZ+B
7zAZSTXh2mukHukRERG1//2FsiBZeapRhAaFWdWnPDN95bg7L7/qQ5szjQARAQAB
tDdWaWt0b3IgQWdleWV2IDxhZ2V5ZXZAaW50ZXJuYXRpb25hbC1hcmJpdHJhdGlv
bi5vcmcudWs+iQI/BBMBAgApBQJWaX9MAhsvBQkB4lLUBwsJCAcDAgEGFQgCCQoL
BBYCAwECHgECF4AACgkQzjaf2edxc+UywQ/+MfYX5BRzl7iSndhr/vVOEycxZntu
9efMhwQYO87EXT/cNpRMX/u2Wzhx70brzFIenvaYTPpVkYKinKv3iEAauJ1wr5/a
PrUxRcfVnBrV8ecWTO6SbNRDePqayst8bBq9rdqnKnpDEv911zk2hjJtCeFDlUkO
eiTLgYCVOQ7n0fbN4pjrBGAqDGs2SFjJXAKYJpZqY77P0crHxXCMyukrdj2WxnB7
JyMdmBqViJSo/MCi7SFXqVQFTvJZVBhTQEUO5KNJA6oAYztYKXBE3AybE528m571
p+HhJ2mOnlNaRu5y12RRQJ7qmKvN9uzHz0+mt8rRk3oYWCYImTMwxDpc1mT39QKs
0WS/zp5NTIaa+Hvt7V8GH5hmq67YL+JcxEINZGZ+MZmiSKuvGVuqC8ZJpswumVw5
PHtMEL+0zBxZXN41YyfTudssXbvMrNiGIdlQA7CW/7yMviVR4nyQtFQJwYSL4mva
uTQSnihm4Bj6FeYpgGpk1TyIzgtIyaQNSvnlDYFezmN/JsAs+25EP/oxyFLaK+ij
WdjVmnsDGS4l5BWH/a8BYPbSnm7YrlYkYCXjTjK1/EeWA/tggApISFptc8YQw0Wp
Qw0IwhJVPWDVW8nDrv/1IPhTJgYfIfdmxr32RLhNePEZGZ02zfCP9BrlNKut9/Hn
x1yKWjVaWZO8VtU=
=XyhJ
-----END PGP PUBLIC KEY BLOCK-----

В таком виде публичный ключ может быть передан в веб-форму. Мы напишем код который может на сервере извлечь данные из ключа, и затем, скажем, вернуть их пользователю (сервис распознавания публичных ключей) и/или сохранить в базе данных. В BC есть класс PGPPublicKey, нашей задачей будет создать новый объект этого класса используя на входе String, а потом используя методы предоставляемые этим классом получить данные о ключе.

В пакете org.bouncycastle.openpgp.examples есть класс PGPExampleUtil, мы не сможем его импортировать так как он непубличный, но его код можем использовать для создания своего аналогичного класса. Метод из PGPExampleUtil который мы можем взять за основу:
/**
     * A simple routine that opens a key ring file and loads the first available key
     * suitable for encryption.
     * 
     * @param input data stream containing the public key data
     * @return the first public key found.
     * @throws IOException
     * @throws PGPException
     */
    static PGPPublicKey readPublicKey(InputStream input) throws IOException, PGPException
    {
        PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
            PGPUtil.getDecoderStream(input), new JcaKeyFingerprintCalculator());

        //
        // we just loop through the collection till we find a key suitable for encryption, in the real
        // world you would probably want to be a bit smarter about this.
        //

        Iterator keyRingIter = pgpPub.getKeyRings();
        while (keyRingIter.hasNext())
        {
            PGPPublicKeyRing keyRing = (PGPPublicKeyRing)keyRingIter.next();

            Iterator keyIter = keyRing.getPublicKeys();
            while (keyIter.hasNext())
            {
                PGPPublicKey key = (PGPPublicKey)keyIter.next();

                if (key.isEncryptionKey())
                {
                    return key;
                }
            }
        }

        throw new IllegalArgumentException("Can't find encryption key in key ring.");
    }


В portablepgp.core.KeyRing похожая задача решается следующим образом:
    public static PGPPublicKeyRing importPublicKey(InputStream iKeyStream) throws IOException {

        PGPPublicKeyRing newKey = new PGPPublicKeyRing(new ArmoredInputStream(iKeyStream));

        pubring = PGPPublicKeyRingCollection.addPublicKeyRing(pubring, newKey);
        savePubring();

        return newKey;
    }

Исходя из предположения что нам нужно вынуть только один (первый) PGPPublicKey из PGPPublicKeyRing просто используем .getPublicKey() и не используем итератор (на входе String, метод возвращает org.bouncycastle.openpgp.PGPPublicKey):
    public static PGPPublicKey importPublicKey (String armoredPublicPGPkeyBlock) throws IOException {

        InputStream in = new ByteArrayInputStream(armoredPublicPGPkeyBlock.getBytes());

        PGPPublicKeyRing pgpPublicKeyRing = new PGPPublicKeyRing(new ArmoredInputStream(in), new JcaKeyFingerprintCalculator());

        in.close();

        PGPPublicKey pgpPublicKey = pgpPublicKeyRing.getPublicKey();

        return pgpPublicKey;
    }

Либо мы можем полностью скопировать в наш класс метод readPublicKey из org.bouncycastle.openpgp.examples.PGPExampleUtil, и дополнить класс следующим методом:
    public static PGPPublicKey readPublicKeyFromString (String armoredPublicPGPkeyBlock) throws IOException, PGPException {
        
        InputStream in = new ByteArrayInputStream(armoredPublicPGPkeyBlock.getBytes());

        PGPPublicKey pgpPublicKey = readPublicKey(in);

        in.close();
        
        return pgpPublicKey;
    }

Отображение данных ключа


На основе org.bouncycastle.openpgp.PGPPublicKey объекта создаем представление данных ключа, которое может быть прочитано человеком или сохранено в базе данных.

Для простейшего варианта представления данных публичного ключа в читаемом виде можно воспользоваться классом portablepgp.core.PrintablePGPPublicKey c методом toString():
package portablepgp.core;

import java.util.Iterator;
import org.bouncycastle.openpgp.PGPPublicKey;

/**
 *
 * @author Primiano Tucci - http://www.primianotucci.com/
 */
public class PrintablePGPPublicKey  {
    PGPPublicKey base;
    
    public PrintablePGPPublicKey(PGPPublicKey iBase){
        base = iBase;
    }
    
    public PGPPublicKey getPublicKey(){
        return base;
    }

    @Override
    public String toString() {
        StringBuilder outStr = new StringBuilder();
        Iterator iter = base.getUserIDs();
        
        outStr.append("[0x");
        outStr.append(Integer.toHexString((int)base.getKeyID()).toUpperCase());
        outStr.append("] ");
        
        while(iter.hasNext()){
            outStr.append(iter.next().toString());
            outStr.append("; ");
        }
        
        return outStr.toString();
    }
        
}

Для хранения более полной информации о ключе создадим класс PGPPublicKeyData с конструктором извлекающим данные из org.bouncycastle.openpgp.PGPPublicKey:

import com.google.common.base.Splitter;
import com.google.gson.Gson;
import org.bouncycastle.openpgp.PGPPublicKey;

import javax.xml.bind.DatatypeConverter;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

public class PGPPublicKeyData {
    String keyID;
    String userID;
    String firstName;
    String lastName;
    String userEmail;
    Date created;
    Date exp;
    int bitStrength;
    String asciiArmored;
    String fingerprint;

    public PGPPublicKeyData(PGPPublicKey pgpPublicKey) {
        // keyID
        StringBuilder keyIDstrBuilder = new StringBuilder();
        keyIDstrBuilder.append("[0x");
        keyIDstrBuilder.append(Integer.toHexString((int) pgpPublicKey.getKeyID()).toUpperCase());
        keyIDstrBuilder.append("] ");
        this.keyID = keyIDstrBuilder.toString();
        // userID
        StringBuilder userIDstrBuilder = new StringBuilder();
        Iterator userIDsIterator = pgpPublicKey.getUserIDs();
        while (userIDsIterator.hasNext()) {
            userIDstrBuilder.append(userIDsIterator.next().toString());
            userIDstrBuilder.append("; ");
        }
        this.userID = userIDstrBuilder.toString();
        // user's first and last name and email
        List<String> userIdList = Splitter.on(' ').trimResults().omitEmptyStrings().splitToList(userID);
        this.firstName = userIdList.get(0);
        this.lastName = userIdList.get(1);
        String userEmailDirty = userIdList.get(userIdList.size() - 1);
        this.userEmail = userEmailDirty.substring(
                1, userEmailDirty.length() - 2
        );
        // fingerprint // [0xE77173E5]  
        this.fingerprint = DatatypeConverter.printHexBinary(
                pgpPublicKey.getFingerprint()
        );
        // creation date
        this.created = pgpPublicKey.getCreationTime();
        // exp date
        this.exp = org.apache.commons.lang3.time.DateUtils.addSeconds(created, (int) pgpPublicKey.getValidSeconds());
        // Bit strength
        this.bitStrength = pgpPublicKey.getBitStrength();
    }

    public String toJSON() {
        Gson gson = new Gson();
        return gson.toJson(this);
    }

    //    ---------- Getters and Setters:

}


Продолжение следует

Комментарии (4)


  1. MaximChistov
    11.01.2016 07:52

    Как-то у вас картинка неправильно сделана, похоже… Насколько я понимаю, после подписи хеш должен поменять свое значение.


    1. demitsuri
      11.01.2016 09:24
      +2

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

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


      1. MaximChistov
        11.01.2016 19:37

        Не понял сначала что «замочек» означает зашифрованность, уж очень гуманитарный способ ее обозначить кмк :)


    1. ageyev
      11.01.2016 14:59

      Хэш не поменял свое значение, просто его зашифровали, и графически это изображено в виде появившегося на хэше замочка :)