Безопасность обмена данными — одно из важнейших свойств современных приложений. С древних времён люди придумывали хитрые способы, которые с развитием человечества стали целой наукой Криптографией. Естественно, Java не осталась в стороне и предложила разработчикам Java Cryptography Architecture (JCA). Данный обзор должен дать первые представления о том, как это работает.
Java Cryptography Architecture: Первое знакомство - 1
Содержание:

Предисловие

Предлагаю перенестись на время в прошлое. Перед нами Древний Рим. И перед нами Гай Юлий Цезарь, который отправляет своим полководцам послание. Давайте посмотрим, что в этом сообщении:
Java Cryptography Architecture: Первое знакомство - 2
Что же может это значить: "ЕСКЕУГЬГМХИФЯ Е УЛП"? Давайте откроем Java Online Compiler, например: 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);
    }
  }
}
Перед нами — простейшая реализация "Шифра Цезаря" (Caesar Cipher). Согласно труду древнеримского историка Светония под названием "Жизнь двенадцати цезарей" именно так Цезарь шифровал послания к своим полководцам. И это - один из самых древних упоминаний использования такой вещи, как Криптография. Слово "криптография" происходит от древнегреческих слов "скрытый" и "пишу", т.е. это наука о методах обеспечения конфиденциальности. В Java есть своя поддержка криптографии и называется она - Java Cryptography Architecture (JCA). Описание можно найти в официальной документации от Oracle — "Java Cryptography Architecture (JCA)". Предлагаю посмотреть, какие возможности мы получаем благодаря JCA.
Java Cryptography Architecture: Первое знакомство - 3

JCA

Как мы уже ранее узнали, в Java для работы с криптографией предлагает Java Cryptography Architecture (JCA). Данная архитектура содержит API (т.е. некоторый набор интерфейсов) и провайдеры (которые их реализуют):
Java Cryptography Architecture: Первое знакомство - 4
Как сказано в документации, "The Java platform includes a number of built-in providers". То есть платформа 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 Cryptography Architecture: Первое знакомство - 5

MessageDigest

Первое про что упоминается в документации JCA — это MessageDigest. Вообще Digest на русском будет так же — дайджест и соответствует по смыслу "краткое изложение". Но в криптографии дайджестом называется хэш-сумма. А ещё можно легко запомнить, что на английском Digest можно перевести ещё как переваривать. Подробнее можно прочитать в документации JCA в разделе "MessageDigest". Как сказано в документации, MessageDigest генерирует фиксированного размера результат, называемый digest или hash. Хэширование — это односторонняя функция, т.е. если мы что-то захэшировали, то из результата (т.е. из хэша) мы не можем получить первоисточник. Но если хэшируется одинаковые объекты (например, строки из одинаковых символов), то их хэш должен совпадать. Как сказано в документации, такой хэш называется иногда ещё "контрольной суммой" (checksums) или "цифровым слепком/отпечатком" "digital fingerprints" данных. Хэширование может выполняться используя разные алгоритмы. Доступные алгоритмы можно посмотреть в документе "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 Cryptography Architecture: Первое знакомство - 6

Симметричное шифрование (symmetric key cryptography)

Симметричное шифрование — это шифрование, при котором для шифрования и дешифрования используется один и тот же ключ. Для того, чтобы использовать симметричное шифрование нам нужен ключ. Чтоб его получить используем KeyGenerator. Кроме того, нам понадобится класс, представляющий собой шифр (Cipher). Как сказано в документации JCA в разделе "Creating a Cipher Object", чтобы создать Cipher нужно указать в строке не просто алгоритм, а "трансформацию". Описание трансформации выглядит следующим образом: "algorithm/mode/padding":
  • Алгоритм: тут смотрим в стандартных именах для "Cipher (Encryption) Algorithms". Рекомендуется использовать AES.
  • Режим: режим шифрования. Например: ECB или CBC (об этом мы поговорим чуть дальше)
  • Отступ/разбивка: каждый блок данных шифруется отдельно. Данный параметр определяет, какой объём данных считать за 1 блок.
Для примера, возьмём следующую трансформацию: "AES/ECB/PKCS5Padding". То есть алгоритм шифрования — AES, режим шифрования ECB (сокращение для Electronic Codebook), размер блока - PKCS5Padding. PKCS5Padding говорит, что размер одного блока - 2 байта (16 бит). Режим шифрования Electronic Codebook предполагает последовательное шифрование каждого из блоков:
Java Cryptography Architecture: Первое знакомство - 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 Cryptography Architecture: Первое знакомство - 8
Чтобы избежать в таком случае повтора следует использовать другой режим — Cipher Block Chaining (CBC). Данный режим вводит понятие Initialization Vector (представлен классом IvParameterSpec). А так же благодаря этому режиму результат генерации прошлого блока будет использован для генерации следующего:
Java Cryptography Architecture: Первое знакомство - 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 советую прочитать материал: "Режим электронной кодовой книги". Но симметричное шифрование имеет очевидную проблему - нужно как-то передать ключ от того, кто шифрует, тому, кто шифрует. И на этом пути этот ключ можно перехватить и тогда будет возможность перехватывать данные. И эту проблему призвано решить ассиметричное шифрование.
Java Cryptography Architecture: Первое знакомство - 10

Ассиметричное шифрование (asymmetric cryptography)

Ассиметричное шифрование или же Public-key cryptography — это такой способ шифрования, при котором используется пара ключей: private key (хранится от всех в секрете) и public 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 используем один ключ для шифрования, а другой — для дешифрования. Но т.к. смысл шифрования в том, чтобы расшифровать смог только получатель, то шифруется публичным ключом, а расшифровывается только приватным.
Java Cryptography Architecture: Первое знакомство - 11

Цифровая подпись

Как мы видели выше, зная public key можно отправлять данные так, чтобы их смог расшифровать только владелец private key. То есть суть ассиметричного шифрования в том, что шифрует кто угодно, а читаем только мы. Есть и обратная процедура - цифровая подпись, представленная классом Signature. Цифровая подпись может использовать следующие алгоритмы: "Signature Algorithms". Документация по JCA предлагает приглядеться к этим двум: DSAwithMD5 и RSAwithMD5 Что лучше DSA или RSA и в чём их отличие можно прочитать здесь: "Which Works Best for Encrypted File Transfers - RSA or DSA?". Или почитать обсуждения здесь: "RSA vs. DSA for SSH authentication keys". Итак, цифровая подпись. Нам потребуется, как и раньше, пара ключей 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 Cryptography Architecture: Первое знакомство - 12
Выше мы увидели, как стороны обмениваются данными. Нет ли какого-то стандартного интерфейса для этого взаимодействия, предусмотренного в JCA? Оказывается - есть. Давайте рассмотрим его.
Java Cryptography Architecture: Первое знакомство - 13

KeyAgreement

Java Cryptography Architecture вводит важный инструмент — Key agreement is a protocol. Представлен он классом 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();

Теперь, у Алисы есть открытый ключ Боба, а у Боба есть открытый ключ Алисы. Что дальше?
Как сказано в документации 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: "Diffie-Hellman Key Exchange between 2 Parties". Примерно так выглядит работа ассиметричного шифрования в исполнении Java Cryptography Architecture с использованием Key agreement protocol'а. Подробнее про ассиметричное шифрование рекомендуются видео:
Java Cryptography Architecture: Первое знакомство - 14

Сертификаты

Ну и на сладкое у нас осталось не менее важное — сертификаты. Обычно сертификаты генерируются при помощи утилиты keytool, входящей в поставку jdk. Подробнее можно прочитать, например, тут: "Generating a self-signed SSL certificate using the Java keytool command". Так же можно прочитать в руководствах от Oracle. Например, тут: "To Use keytool to Create a Server Certificate". Для примера воспользуемся 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 Public Key Infrastructure". Вот пример:
// 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, т.е. имя того, с чем ассоциирован публичный ключ
// 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 Cryptography Architecture: Первое знакомство - 15

Хранилище ключей (KeyStore)

Последнее, о чём хотелось бы поговорить, это о хранилище ключей и сертификатов, которое называется KeyStore. Понятно, что постоянно генерировать сертификаты и ключи — дорого и бессмысленно. Поэтому их надо как-то безопасно хранить. Для этого есть средство — KeyStore. Хранилище ключей описано в документации JCA в главе "KeyManagement". 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 создаётся пустой, чтобы его дальше сохранить. Второй параметр тоже null, т.к. мы создаём новый KeyStore. Если бы мы загружали KeyStore из файла, то тут нужно было бы указать пароль (по аналогии с методом KeyStore с названием store).
Java Cryptography Architecture: Первое знакомство - 16

Итог

Вот мы и рассмотрели с Вами самые основные и элементарные действия в рамках Java Cryptography Architecture (оно же JCA). Мы увидели, что такое симметричное и ассиметричное шифрование и как это реализовано в JCA. Мы увидели, как создаются сертификаты и цифровые подписи, а так же как используются. Это всё лишь основы основ, за которыми стоит ещё много сложных и интересных вещей. Надеюсь, данные обзорный материал будет полезен и заинтересует Вас на дальнейшее изучение данного направления. Ну и в заключении хотелось бы указать супер видео по данной теме:
Java Cryptography Architecture: Первое знакомство - 17
Дополнительные материалы: