JavaRush /Java Blog /Random-TW /Java 密碼學架構:初步介紹
Viacheslav
等級 3

Java 密碼學架構:初步介紹

在 Random-TW 群組發布
資料交換的安全性是現代應用程式最重要的屬性之一。自古以來,人們就想出了巧妙的方法,隨著人類的發展,密碼學成為一門完整的科學。當然,Java 並沒有袖手旁觀,而是為開發人員提供了 Java 密碼體系結構 (JCA)。這篇評論應該讓您初步了解它是如何運作的。

前言

我建議回到過去。擺在我們面前的是古羅馬。在我們面前的是蓋烏斯·朱利葉斯·凱撒,他向他的指揮官們發送了一條訊息。我們來看看這則訊息的內容:
Java 密碼學架構:初步介紹 - 2
這可能意味著什麼:"ЕСКЕУГЬГМХИФЯ Е УЛП"?我們開啟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);
    }
  }
}
擺在我們面前的是凱撒密碼最簡單的實作。根據古羅馬歷史學家蘇托尼烏斯(Suetonius)題為《十二位凱撒的傳記》的著作,凱撒正是透過這種方式向他的將軍們加密訊息。這是對密碼學之類的使用的最古老的參考之一。“密碼學”一詞源自古希臘語“隱藏”和“寫入”,即 這是隱私技術的科學。Java 有自己的密碼學支持,稱為Java 密碼學體系架構(JCA)。描述可以在Oracle的官方文件-《Java Cryptography Architecture (JCA)》中找到。我建議您看看我們透過 JCA 獲得了哪些機會。
Java 密碼學架構:初步介紹 - 3

J.C.A.

正如我們之前所了解的,Java 提供了 Java 密碼體系結構 (JCA) 來處理密碼學。該架構包含一個 API(即一組特定的介面)和提供者(實作它們):
Java 密碼學架構:初步介紹 - 4
如文件所述,「Java 平台包含許多內建提供者」。也就是說,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 密碼學架構:初步介紹 - 5

資訊摘要

JCA文件中首先提到的是MessageDigest。一般來說,俄語中的「摘要」是相同的——「摘要」的意思相當於「摘要」。但在密碼學中,摘要是哈希和。你也可以很容易記住,在英文中Digest也可以翻譯為摘要。更多詳細資訊可以在 JCA 文件的“ MessageDigest ”部分中找到。如文件所述,MessageDigest 產生一個固定大小的結果,稱為摘要或雜湊。雜湊是一種單向函數,即 如果我們對某些內容進行散列,那麼從結果(即散列)中我們無法獲得原始來源。但如果相同的物件被雜湊(例如,相同字元的字串),那麼它們的雜湊必須匹配。如文件中所述,此類雜湊有時也稱為資料的「校驗和」或「數位指紋」。可以使用不同的演算法來執行雜湊。可用的演算法可以查看文件《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 密碼學架構:初步介紹 - 6

對稱密鑰密碼學

對稱加密是使用相同金鑰進行加密和解密的加密。為了使用對稱加密,我們需要一個金鑰。為了獲得它,我們使用KeyGenerator。此外,我們還需要一個表示密碼(Cipher)的類別。正如 JCA 文件中「建立密碼物件」部分所述,要建立密碼,您不僅需要指定演算法,還需要在行中指定「轉換」。轉換描述如下所示:「演算法/模式/填充」:
  • 演算法:這裡我們來看看「密碼(加密)演算法」的標準名稱。推薦使用AES。
  • 模式:加密模式。例如:ECB 或 CBC(我們稍後會討論這個)
  • 縮排/分割:每個資料塊都單獨加密。此參數決定多少資料算 1 個區塊。
例如,採用以下轉換:"AES/ECB/PKCS5Padding"。即加密演算法為AES,加密方式為ECB(Electronic Codebook的縮寫),區塊大小為PKCS5Padding。PKCS5Padding 表示一個區塊的大小為 2 位元組(16 位元)。電子密碼本加密模式涉及每個區塊的順序加密:
Java 密碼學架構:初步介紹 - 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 密碼學架構:初步介紹 - 8
為了避免在這種情況下重播,您應該使用另一種模式 - 密碼塊連結 (CBC)。此模式引入了初始化向量的概念(由 IvParameterSpec 類別表示)。也得益於這種模式,產生最後一個區塊的結果將用於產生下一個區塊:
Java 密碼學架構:初步介紹 - 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的更多信息,我建議您閱讀資料:“電子密碼本模式”。但對稱加密有一個明顯的問題——你需要以某種方式將金鑰從加密者轉移到加密者。而沿著這個路徑,這個key就可以被截獲,那麼就有可能截獲資料。而非對稱加密就是為了解決這個問題而設計的。
Java 密碼學架構:初步介紹 - 10

非對稱加密

非對稱加密或公鑰加密是一種使用一對金鑰的加密方法:私鑰(對所有人保密)和公鑰(公眾可用)。為了在資訊交換各方之間安全地交換公鑰,同時確保秘密金鑰的安全,這種分離是必要的。在建立密鑰對時,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 密碼學架構:初識 - 11

電子簽名

正如我們在上面看到的,知道公鑰,您可以發送數據,以便只有私鑰的所有者才能解密它。也就是說,非對稱加密的本質是任何人都加密,但只有我們在讀取。還有一個相反的過程 - 數位簽名,由Signature類別表示。數位簽章可以使用以下演算法:「簽章演算法」。JCA 文件建議仔細研究這兩個:DSAwithMD5 和 RSAwithMD5 比 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 密碼學架構:初識 - 12
上面我們看到了各方如何交換資料。JCA 中是否沒有提供一些用於此互動的標準介面?事實證明是有的。我們來看一下。
Java 密碼學架構:初識 - 13

關鍵協議

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 金鑰交換」。這大致上就是使用金鑰協商協定的 Java 加密體系結構中的非對稱加密的樣子。有關非對稱加密的更多信息,推薦視頻:
Java 密碼學架構:初識 - 14

證書

好吧,對於甜點,我們還有同樣重要的東西——證書。通常,憑證是使用 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 規範的使用方式清晰可見:「Internet 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);
}
Java 密碼學架構:初步介紹 - 15

金鑰庫(KeyStore)

最後我要講的是金鑰和憑證存儲,稱為KeyStore。顯然,不斷產生憑證和金鑰是昂貴且毫無意義的。因此,它們需要以某種方式安全地儲存。有一個工具可以用於此目的 - KeyStore。密鑰儲存在 JCA 文件的「密鑰管理」一章中進行了描述。使用它的 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 被創建為空,以便進一步保存。第二個參數也為空,因為 我們正在建立一個新的金鑰庫。如果我們從檔案載入 KeyStore,那麼我們需要在此指定一個密碼(類似於名為 store 的 KeyStore 方法)。

底線

因此,我們與您一起回顧了 Java 加密體系結構(又稱 JCA)框架內最基本且最基本的操作。我們了解了什麼是對稱加密和非對稱加密以及它是如何在 JCA 中實現的。我們了解如何建立憑證和數位簽章以及如何使用它們。這些都只是基礎,背後還有很多更複雜、更有趣的事。我希望這篇綜述資料對您有用,並使您對進一步研究該領域感興趣。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION