A segurança da troca de dados é uma das propriedades mais importantes das aplicações modernas. Desde os tempos antigos, as pessoas criaram métodos astutos, que com o desenvolvimento da humanidade se tornaram toda a ciência da criptografia. Naturalmente, Java não ficou de lado e ofereceu aos desenvolvedores a Java Cryptography Architecture (JCA). Esta revisão deve dar uma primeira ideia de como funciona.
O que isso poderia significar:
Como diz a documentação, “ A plataforma Java inclui vários provedores integrados ”. Ou seja, a plataforma Java fornece um conjunto de provedores integrados que podem ser expandidos se necessário. Você pode ver isso por si mesmo:
Pode ficar assim no código:
Para evitar a repetição neste caso, você deve usar outro modo - Cipher Block Chaining (CBC). Este modo introduz o conceito de Vetor de Inicialização (representado pela classe IvParameterSpec). E também graças a este modo, o resultado da geração do último bloco será utilizado para gerar o próximo:
Vamos agora escrever isso em código:
Acima vimos como as partes trocam dados. Não existe alguma interface padrão para esta interação fornecida no JCA? Acontece que existe. Vamos dar uma olhada nisso.
Prefácio
Proponho viajar no tempo. Diante de nós está a Roma Antiga. E diante de nós está Caio Júlio César, que envia uma mensagem aos seus comandantes. Vamos ver o que há nesta mensagem:"ЕСКЕУГЬГМХИФЯ Е УЛП"
? Vamos abrir o Java Online Compiler, por exemplo: 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);
}
}
}
Diante de nós está a implementação mais simples da Cifra de César. De acordo com o trabalho do antigo historiador romano Suetônio, intitulado “As Vidas dos Doze Césares”, foi exatamente assim que César criptografou mensagens para seus generais. E esta é uma das referências mais antigas ao uso de algo como criptografia . A palavra "criptografia" vem das antigas palavras gregas "oculto" e "escrever", ou seja, é a ciência das técnicas de privacidade. Java tem seu próprio suporte para criptografia e é chamado Java Cryptography Architecture (JCA). A descrição pode ser encontrada na documentação oficial da Oracle - " Java Cryptography Architecture (JCA) ". Sugiro que você veja quais oportunidades temos graças à JCA.
JCA
Como aprendemos anteriormente, Java oferece a Java Cryptography Architecture (JCA) para trabalhar com criptografia. Esta arquitetura contém uma API (ou seja, um determinado conjunto de interfaces) e provedores (que as implementam):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());
}
}
}
Registrar um provedor terceirizado é muito fácil. Por exemplo: Security.addProvider(new BouncyCastleProvider());
Este exemplo conecta um dos provedores mais famosos - BouncyCastle . Mas nesta revisão usaremos apenas ferramentas básicas, sem bibliotecas de terceiros. Nosso documento principal: " Java Cryptography Architecture (JCA) ". Compreender como o JCA funciona ajudará você a entender mais facilmente as tecnologias nas quais esse mesmo JCA é usado ativamente. Por exemplo: HTTPS (veja " De HTTP para HTTPS ").
MensagemDigest
A primeira coisa mencionada na documentação JCA é MessageDigest. Em geral, o Digest em russo será o mesmo - um resumo corresponde em significado a “um resumo”. Mas em criptografia, um resumo é uma soma hash. Você também pode lembrar facilmente que em inglês Digest também pode ser traduzido como resumo. Mais detalhes podem ser encontrados na documentação do JCA na seção " MessageDigest ". Como diz a documentação, MessageDigest gera um resultado de tamanho fixo chamado digest ou hash. Hashing é uma função unilateral, ou seja, se fizermos hash de algo, então a partir do resultado (ou seja, do hash) não poderemos obter a fonte original. Mas se objetos idênticos forem hash (por exemplo, strings de caracteres idênticos), então seu hash deverá corresponder. Conforme declarado na documentação, esse hash às vezes também é chamado de “soma de verificação” ou “impressão digital” de dados. O hash pode ser realizado usando diferentes algoritmos. Os algoritmos disponíveis podem ser visualizados no documento " Java Cryptography Architecture Standard Algorithm Name Documentation for JDK 8 ". Vamos fazer o hash e imprimir o hash no console: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);
}
}
}
O hash pode ser útil, por exemplo, ao armazenar senhas. Já o hash da senha inserida pode ser verificado em relação a um hash salvo anteriormente. Se os hashes corresponderem, a senha também corresponderá. Para um hashing ainda mais seguro, é usado um conceito chamado “salt”. Salt pode ser implementado usando a classe SecureRandom . Antes de executar o método digest, vamos descrever a adição de "sal":
byte[] salt = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(salt);
digester.update(salt);
Mas hash é uma função unilateral. Mas e se você quiser criptografar e descriptografar?
Criptografia de chave simétrica
A criptografia simétrica é a criptografia que usa a mesma chave para criptografar e descriptografar. Para usar criptografia simétrica, precisamos de uma chave. Para obtê-lo, usamos KeyGenerator . Além disso, precisaremos de uma classe que represente uma cifra ( Cipher ). Conforme declarado na documentação do JCA na seção “ Criando um Objeto Cifra ”, para criar uma Cifra é necessário especificar não apenas um algoritmo, mas uma “transformação” na linha. A descrição da transformação é semelhante a esta: "algoritmo/modo/preenchimento":- Algoritmo : aqui vemos os nomes padrão para “ Algoritmos de Cifra (Criptografia) ”. Recomenda-se usar AES.
- Modo : modo de criptografia. Por exemplo: BCE ou CBC (falaremos sobre isso um pouco mais tarde)
- Recuo/Divisão : Cada bloco de dados é criptografado separadamente. Este parâmetro determina quantos dados são contados como 1 bloco.
"AES/ECB/PKCS5Padding"
. Ou seja, o algoritmo de criptografia é AES, o modo de criptografia é ECB (abreviação de Electronic Codebook), o tamanho do bloco é PKCS5Padding. PKCS5Padding diz que o tamanho de um bloco é de 2 bytes (16 bits). O modo de criptografia do Electronic Codebook envolve a criptografia sequencial de cada bloco:
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);
}
}
Se executarmos, esperaremos uma repetição, porque especificamos 32 caracteres. Esses caracteres constituem 2 blocos de 16 bits:
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);
}
}
Como podemos ver, como resultado não vemos blocos de cifras repetidos. Por esta razão, o modo BCE não é recomendado, porque torna possível ver repetições e usar esse conhecimento para descriptografia. Para mais informações sobre BCE e CBC, aconselho a leitura do material: “ Modo livro de código eletrônico ”. Mas a criptografia simétrica tem um problema óbvio - você precisa de alguma forma transferir a chave de quem criptografa para quem criptografa. E nesse caminho essa chave pode ser interceptada e então será possível interceptar dados. E a criptografia assimétrica foi projetada para resolver esse problema.
Criptografia assimétrica
A criptografia assimétrica ou criptografia de chave pública é um método de criptografia que usa um par de chaves: uma chave privada (mantida em segredo de todos) e uma chave pública (disponível ao público). Esta separação é necessária para a troca segura da chave pública entre as partes na troca de informações, mantendo a chave secreta segura. Ao criar um par de chaves, KeyGenerator não é mais suficiente para nós; precisamos de KeyPairGenerator . Vejamos um exemplo: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));
}
}
É importante entender aqui que ao usar criptografia assimétrica, sempre utilizamos KeyPair para usar uma chave para criptografia e outra para descriptografia. Mas porque O objetivo da criptografia é que apenas o destinatário pode descriptografá-la; ela é criptografada com uma chave pública e descriptografada apenas com uma chave privada.
Assinatura digital
Como vimos acima, conhecendo a chave pública, você pode enviar dados para que somente o dono da chave privada possa descriptografá-los. Ou seja, a essência da criptografia assimétrica é que qualquer um criptografa, mas apenas nós lemos. Existe também um procedimento inverso - uma assinatura digital, representada pela classe Signature . Uma assinatura digital pode usar os seguintes algoritmos: “ Algoritmos de Assinatura ”. A documentação do JCA sugere dar uma olhada nesses dois: DSAwithMD5 e RSAwithMD5 O que é melhor que DSA ou RSA e qual a diferença deles você pode ler aqui: "Qual funciona melhor para transferências de arquivos criptografados - RSA ou DSA? ". Ou leia as discussões aqui: " RSA vs. DSA para chaves de autenticação SSH ". Então, assinatura digital. Precisaremos, como antes, de um KeyPair e de uma nova classe Signature. Se você já testou em compiladores online, o exemplo a seguir pode ser um pouco difícil para eles. Meu exemplo só foi executado aqui: rextester.com . Importamos as classes que precisamos:import javax.crypto.*;
import java.security.*;
Também reescreveremos o método principal:
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));
}
}
É assim que funciona uma assinatura digital. A assinatura digital é um tema interessante. Aconselho você a dar uma olhada no relatório sobre este assunto:
Acordo-chave
A arquitetura de criptografia Java apresenta uma ferramenta importante - o acordo de chave é um protocolo. É representado pela classe KeyAgreement . Conforme declarado na documentação da JCA, este protocolo permite que várias partes definam a mesma chave criptográfica sem compartilhar qualquer informação secreta entre as partes. Soa estranho? Então vejamos um exemplo:// 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));
Este exemplo foi retirado do exemplo de documentação JCA: " Diffie-Hellman Key Exchange entre 2 partes ". Esta é aproximadamente a aparência da criptografia assimétrica na arquitetura de criptografia Java usando o protocolo de acordo de chave. Para mais informações sobre criptografia assimétrica, vídeos recomendados:
Certificados
Pois bem, de sobremesa ainda temos algo não menos importante - os certificados. Normalmente, os certificados são gerados usando o utilitário keytool incluído no jdk. Você pode ler mais detalhes, por exemplo, aqui: " Gerando um certificado SSL autoassinado usando o comando Java keytool ". Você também pode ler os manuais da Oracle. Por exemplo, aqui: " Para usar o keytool para criar um certificado de servidor ". Por exemplo, vamos usar o 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));
}
}
Como podemos ver, um certificado oferece a capacidade de fornecer uma chave pública. Este método tem uma desvantagem - usamos sun.security
, o que é considerado arriscado, porque... este pacote não faz parte da API Java pública. É por isso que durante a compilação é necessário especificar o parâmetro - XDignore.symbol.file
. Existe outra maneira - criar um certificado manualmente. A desvantagem é que ele usa uma API interna que não está documentada. No entanto, é útil saber sobre isso. No mínimo, porque é claramente visível como a especificação RFC-2459 é utilizada: “ Internet X.509 Public Key Infrastructure ”. Aqui está um exemplo:
// 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);
}
Armazenamento de chaves (KeyStore)
A última coisa sobre a qual gostaria de falar é o armazenamento de chaves e certificados, chamado KeyStore. É claro que a geração constante de certificados e chaves é cara e inútil. Portanto, eles precisam ser armazenados de forma segura. Existe uma ferramenta para isso - KeyStore. O armazenamento de chaves é descrito na documentação JCA no capítulo " KeyManagement ". A API para trabalhar com isso é muito clara. Aqui está um pequeno exemplo: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());
}
Como você pode ver no exemplo, ele é executado primeiro load
para o KeyStore. Mas no nosso caso, especificamos o primeiro atributo como nulo, ou seja, não há fonte para KeyStore. Isso significa que o KeyStore é criado vazio para salvá-lo ainda mais. O segundo parâmetro também é nulo, porque estamos criando um novo KeyStore. Se estivéssemos carregando o KeyStore de um arquivo, precisaríamos especificar uma senha aqui (semelhante ao método KeyStore chamado store).
GO TO FULL VERSION