データ交換のセキュリティは、最新のアプリケーションの最も重要な特性の 1 つです。古代以来、人々は狡猾な方法を考え出し、人類の発展とともにそれが暗号科学の全体となりました。当然のことながら、Java は脇に立つことはなく、開発者に Java 暗号化アーキテクチャ (JCA) を提供しました。このレビューでは、それがどのように機能するかを最初に理解できるはずです。
これは何を意味しますか
ドキュメントにあるように、「Java プラットフォームには多数の組み込みプロバイダーが含まれています」。つまり、Java プラットフォームは、必要に応じて拡張できる一連の組み込みプロバイダーを提供します。これは自分で確認できます。
コードでは次のようになります。
この場合の再実行を回避するには、別のモード、Cipher Block Chaining (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);
}
}
}
私たちの前にあるのは、シーザー暗号の最も単純な実装です。古代ローマの歴史家スエトニウスの著書「12人のカエサルの生涯」によれば、これはまさにカエサルが将軍へのメッセージを暗号化した方法だという。そして、これは、暗号などの使用に関する最も古い言及の 1 つです。「暗号」という言葉は、古代ギリシャ語の「隠す」と「書く」という言葉に由来しています。それはプライバシー技術の科学です。Java には独自の暗号化サポートがあり、 Java 暗号化アーキテクチャ(JCA)と呼ばれます。この説明は、Oracle の公式ドキュメント「Java Cryptography Architecture (JCA)」に記載されています。JCA のおかげで私たちがどのような機会を得られるのか見てみることをお勧めします。
J.C.A.
以前に学んだように、Java は暗号化を操作するための Java Cryptography Architecture (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());
この例では、最も有名なプロバイダーの 1 つであるBouncyCastleに接続します。ただし、このレビューでは、サードパーティのライブラリを使用せず、基本的なツールのみを使用します。私たちのメインドキュメント: 「Java 暗号化アーキテクチャ (JCA)」。JCA がどのように機能するかを理解すると、同じ JCA が積極的に使用されているテクノロジをより簡単に理解できるようになります。例: HTTPS (「HTTP から HTTPS へ」を参照)。
メッセージダイジェスト
JCA ドキュメントで最初に言及されているのは MessageDigest です。一般に、ロシア語のダイジェストも同じです。ダイジェストは「要約」という意味に相当します。しかし、暗号化では、ダイジェストはハッシュサムです。英語ではDigestはダイジェストとも訳せることも覚えておくと良いでしょう。詳細については、JCA ドキュメントの「MessageDigest」セクションを参照してください。ドキュメントに記載されているように、MessageDigest はダイジェストまたはハッシュと呼ばれる固定サイズの結果を生成します。ハッシュは一方向関数です。つまり、何かをハッシュした場合、結果から(つまりハッシュから)元のソースを取得することはできません。ただし、同一のオブジェクトがハッシュされる場合 (たとえば、同一の文字列)、それらのハッシュは一致する必要があります。ドキュメントに記載されているように、このようなハッシュはデータの「チェックサム」または「デジタル指紋」とも呼ばれます。ハッシュはさまざまなアルゴリズムを使用して実行できます。利用可能なアルゴリズムは、ドキュメント「JDK 8 の Java 暗号化アーキテクチャ標準アルゴリズム名ドキュメント」で参照できます。ハッシュを実行して、そのハッシュをコンソールに出力しましょう。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クラスを使用して実装できます。ダイジェスト メソッドを実行する前に、「ソルト」の追加について説明します。
byte[] salt = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(salt);
digester.update(salt);
しかし、ハッシュは一方向関数です。しかし、暗号化と復号化ができるようにしたい場合はどうすればよいでしょうか?
対称鍵暗号化
対称暗号化は、暗号化と復号化に同じキーを使用する暗号化です。対称暗号化を使用するには、キーが必要です。これを取得するには、 KeyGeneratorを使用します。さらに、暗号を表すクラス ( Cipher ) が必要になります。JCA ドキュメントの「暗号オブジェクトの作成」セクションに記載されているように、暗号を作成するには、アルゴリズムだけでなく、その行で「変換」も指定する必要があります。変換の説明は次のようになります: "algorithm/mode/padding":- アルゴリズム: ここでは「 Cipher (暗号化) アルゴリズム」の標準名を見ていきます。AES の使用をお勧めします。
- モード: 暗号化モード。例: ECB または CBC (これについては後で説明します)
- インデント/分割: データの各ブロックは個別に暗号化されます。このパラメータは、どのくらいのデータを 1 ブロックとしてカウントするかを決定します。
"AES/ECB/PKCS5Padding"
。つまり、暗号化アルゴリズムは AES、暗号化モードは ECB (Electronic Codebook の略)、ブロック サイズは PKCS5Padding です。PKCS5Padding は、1 ブロックのサイズが 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 文字を指定しました。これらの文字は、16 ビットの 2 つのブロックを構成します。
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 の詳細については、「電子コードブック モード」という資料を読むことをお勧めします。しかし、対称暗号化には明らかな問題があります。何らかの方法で、暗号化する側から暗号化する側にキーを転送する必要があります。そして、このパスに沿って、このキーが傍受され、データが傍受される可能性があります。そして、非対称暗号化はこの問題を解決するために設計されています。
非対称暗号化
非対称暗号化または公開キー暗号化は、秘密キー (誰からも秘密にされる) と公開キー (公開キー) のペアのキーを使用する暗号化方式です。この分離は、秘密鍵を安全に保ちながら、情報交換の当事者間で公開鍵を安全に交換するために必要です。キーペアを作成する場合、 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 を使用して、暗号化に 1 つのキーを使用し、復号化に別のキーを使用することを理解することが重要です。しかし理由は 暗号化のポイントは、受信者だけが暗号化を復号化できることです。暗号化は公開鍵で暗号化され、秘密鍵でのみ復号化されます。
デジタル署名
上で見たように、公開キーがわかれば、秘密キーの所有者だけがデータを復号できるようにデータを送信できます。つまり、非対称暗号化の本質は、誰でも暗号化できますが、読み取るのは私たちだけであるということです。逆の手順、つまりSignatureクラスで表されるデジタル署名もあります。デジタル署名では、「署名アルゴリズム」のアルゴリズムを使用できます。JCA ドキュメントでは、DSAwithMD5 と RSAwithMD5 の 2 つについて詳しく調べることを提案しています。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 Key Exchange」から抜粋したものです。これは、鍵合意プロトコルを使用した 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 仕様「インターネット 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 というツールがあります。キー ストアについては、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 のソースはありません。これは、キーストアをさらに保存するために空で作成されることを意味します。2 番目のパラメータも null です。新しいキーストアを作成しています。ファイルから KeyStore をロードする場合は、ここでパスワードを指定する必要があります (store と呼ばれる KeyStore メソッドと同様)。
GO TO FULL VERSION