Hola Habr! Les presento la traducción del artículo "Criptografía Java" de Jakob Jenkov.
Esta publicación es una traducción del primer artículo de criptografía de Java de una serie de artículos para principiantes que desean aprender los conceptos básicos de la criptografía en Java.
Tabla de contenido:
- Criptografía Java
- Cifrado
- Messagedigest
- Mac
- Firma
- Par de llaves
- Generador de claves
- KeyPairGenerator
- Keystore
- Keytool
- Certificado
- CertificateFactory
- CertPath
Criptografía Java
La API de criptografía de Java proporciona la capacidad de cifrar y descifrar datos en Java, así como administrar claves, firmas y autenticar mensajes (autenticar), calcular hashes criptográficos y mucho más.
Este artículo explica los conceptos básicos sobre cómo utilizar la API de criptografía de Java para realizar diversas tareas que requieren cifrado seguro.
Este artículo no explica los conceptos básicos de la teoría criptográfica. Tendrá que ver esta información en otro lugar.
Extensión de criptografía de Java
La API de criptografía de Java es proporcionada por la llamada Java Cryptography Extension (JCE). JCE ha sido durante mucho tiempo parte de la plataforma Java. Inicialmente, JCE se separó de Java debido a restricciones de exportación en tecnología de cifrado en los Estados Unidos. Por lo tanto, los algoritmos de cifrado más fuertes no se incluyeron en la plataforma estándar de Java. Estos algoritmos de cifrado más sólidos se pueden aplicar si su empresa se encuentra en los EE. UU., Pero en otros casos tendrá que usar algoritmos más débiles o implementar sus propios algoritmos de cifrado y conectarlos a JCE.
Desde 2017, las reglas para exportar algoritmos de cifrado en los Estados Unidos se han relajado significativamente, y en la mayoría de las partes del mundo puede usar estándares de cifrado internacionales a través de Java JCE.
Arquitectura de criptografía de Java
Java Cryptography Architecture (JCA) es el nombre del diseño interno de API de criptografía en Java. JCA se estructura en torno a varias clases principales e interfaces de propósito general. Los proveedores proporcionan la funcionalidad real de estas interfaces. Por lo tanto, puede usar la clase Cipher para cifrar y descifrar algunos datos, pero la implementación específica del cifrado (algoritmo de cifrado) depende del proveedor particular utilizado.
También puede implementar y conectar sus propios proveedores, pero debe tener cuidado con esto. ¡Implementar correctamente el cifrado sin agujeros de seguridad es difícil! Si no sabe lo que está haciendo, probablemente sea mejor usar el proveedor de Java incorporado o un proveedor de confianza como Bouncy Castle.
Principales clases e interfaces
La API de criptografía de Java consta de los siguientes paquetes de Java:
- java.security
- java.security.cert
- java.security.spec
- java.security.interfaces
- javax.crypto
- javax.crypto.spec
- javax.crypto.interfaces
Las principales clases e interfaces de estos paquetes:
- Proveedor
- SecureRandom
- Cifrado
- Messagedigest
- Firma
- Mac
- Algoritmo Parámetros
- AlgorithmParameterGenerator
- Keyfactory
- SecretKeyFactory
- KeyPairGenerator
- Generador de claves
- Keyagreement
- Keystore
- CertificateFactory
- CertPathBuilder
- CertPathValidator
- CertStore
Proveedor
La clase de proveedor (java.security.Provider) es la clase central en la API de cifrado de Java. Para utilizar la API criptográfica de Java, debe instalar un proveedor de criptografía. El SDK de Java viene con su propio proveedor de criptografía. A menos que establezca explícitamente el proveedor de criptografía, se utilizará el proveedor predeterminado. Sin embargo, este proveedor criptográfico puede no admitir los algoritmos de cifrado que desea utilizar. Por lo tanto, es posible que deba instalar su propio proveedor de criptografía.
Uno de los proveedores de criptografía más populares para la API de criptografía Java se llama Bouncy Castle. Aquí hay un ejemplo en el que BouncyCastleProvider está configurado como proveedor criptográfico:
import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; public class ProviderExample { public static void main(String[] args) { Security.addProvider(new BouncyCastleProvider()); } }
Cifrado
La clase Cipher (javax.crypto.Cipher) representa un algoritmo criptográfico. Un cifrado se puede usar tanto para el cifrado como para descifrar datos. La clase Cipher se explica con más detalle en las siguientes secciones, con una breve descripción a continuación.
Crear una instancia de la clase de cifrado que utiliza el algoritmo de cifrado AES para uso interno:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
El método Cipher.getInstance (...) acepta una cadena que determina qué algoritmo de cifrado usar, así como algunos otros parámetros del algoritmo.
En el ejemplo anterior:
- AES - algoritmo de cifrado
- CBC es un modo en el que el algoritmo AES puede funcionar.
- PKCS5Padding es cómo el algoritmo AES debe procesar los últimos bytes de datos para el cifrado. ¿Qué significa exactamente esto? Busque en el manual de criptografía en su conjunto y no en este artículo.
Inicialización de cifrado
Antes de usar una instancia de cifrado, debe inicializarla. La instancia de cifrado se inicializa llamando al método init () . El método init () toma dos parámetros:
- Modo: cifrado / descifrado
- Clave
El primer parámetro indica el modo de operación de la instancia de cifrado: para cifrar o descifrar datos. El segundo parámetro indica qué clave usan para cifrar o descifrar datos.
Un ejemplo:
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}; String algorithm = "RawBytes"; SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm); cipher.init(Cipher.ENCRYPT_MODE, key);
Tenga en cuenta que el método de creación de claves en este ejemplo no es seguro y no debe usarse en la práctica. Este artículo en las siguientes secciones explicará cómo crear claves de manera más segura.
Para inicializar una instancia de cifrado para descifrar datos, debe usar Cipher.DECRYPT_MODE, por ejemplo:
cipher.init(Cipher.DECRYPT_MODE, key);
Cifrado o descifrado de datos
Después de inicializar el cifrado, puede comenzar a cifrar o descifrar los datos llamando a los métodos update () o doFinal () . El método update () se usa si está encriptando o desencriptando un dato. Se llama al método doFinal () cuando encripta la última pieza de datos o si el bloque de datos que pasa a doFinal () es un conjunto único de datos para encriptar.
Un ejemplo de encriptación de datos usando el método doFinal () :
byte[] plainText = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8"); byte[] cipherText = cipher.doFinal(plainText);
Para descifrar los datos, debe pasar el texto cifrado (datos) al método doFinal () o doUpdate () .
Llaves
Para cifrar o descifrar datos, necesita una clave. Existen dos tipos de claves, según el tipo de algoritmo de cifrado que se use:
- Llaves simétricas
- Llaves asimétricas
Las claves simétricas se utilizan para algoritmos de cifrado simétricos. Los algoritmos de cifrado simétrico usan la misma clave para el cifrado y descifrado.
Las claves asimétricas se utilizan para algoritmos de cifrado asimétricos. Los algoritmos de cifrado asimétrico usan una clave para el cifrado y otra para el descifrado. Los algoritmos de cifrado de clave pública y privada son ejemplos de algoritmos de cifrado asimétrico.
De alguna manera, la parte que necesita descifrar los datos debe conocer la clave necesaria para descifrar los datos. Si el descifrador no es parte del cifrado de datos, las dos partes deben acordar una clave o intercambiar una clave. Esto se llama intercambio de claves.
Seguridad clave
Las claves deben ser difíciles de adivinar para que un atacante no pueda recoger fácilmente una clave de cifrado. En el ejemplo de la sección anterior sobre la clase Cipher, se utilizó una clave muy simple y codificada. En la práctica, esto no vale la pena hacerlo. Si la clave de las partes es fácil de adivinar, será fácil para un atacante descifrar los datos cifrados y posiblemente crear mensajes falsos por su cuenta. Es importante hacer una clave que sea difícil de adivinar. Por lo tanto, la clave debe consistir en bytes aleatorios. Cuantos más bytes aleatorios, más difícil es adivinar, porque hay más combinaciones posibles.
Generación clave
Para generar claves de cifrado aleatorias, puede usar la clase Java KeyGenerator. KeyGenerator se describirá con más detalle en los siguientes capítulos, aquí hay un pequeño ejemplo de su uso aquí:
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); SecureRandom secureRandom = new SecureRandom(); int keyBitSize = 256; keyGenerator.init(keyBitSize, secureRandom); SecretKey secretKey = keyGenerator.generateKey();
La instancia de SecretKey resultante se puede pasar al método Cipher.init () , por ejemplo, así:
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
Generación de pares de claves
Los algoritmos de cifrado asimétrico utilizan un par de claves que consta de una clave pública y una clave privada para cifrar y descifrar datos. Para crear un par de claves asimétricas, puede usar KeyPairGenerator (java.security.KeyPairGenerator). KeyPairGenerator se describirá con más detalle en los siguientes capítulos, a continuación se muestra un ejemplo simple de Java KeyPairGenerator:
SecureRandom secureRandom = new SecureRandom(); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); KeyPair keyPair = keyPairGenerator.generateKeyPair();
Tienda de llaves
Java KeyStore es una base de datos que puede contener claves. Java KeyStore está representado por la clase KeyStore (java.security.KeyStore). Un almacén de claves puede contener claves de los siguientes tipos:
- Claves privadas
- Claves públicas y certificados (Claves públicas + certificados)
- Llaves secretas
Las claves privadas y públicas se utilizan en cifrado asimétrico. La clave pública puede tener un certificado asociado. Un certificado es un documento que prueba la identidad de una persona, organización o dispositivo que afirma poseer una clave pública.
El certificado suele estar firmado digitalmente por la parte que confía como prueba.
Las claves privadas se utilizan en el cifrado simétrico. La clase KeyStore es bastante compleja, por lo que se describe con más detalle más adelante en un capítulo separado sobre Java KeyStore.
Java Keytool es una herramienta de línea de comandos que puede funcionar con archivos Java KeyStore. Keytool puede generar pares de claves en un archivo KeyStore, exportar certificados e importar certificados en KeyStore y algunas otras funciones. Keytool viene con una instalación de Java. Keytool se describe con más detalle más adelante en un capítulo separado sobre Java Keytool.
Resumen del mensaje
Cuando recibe datos cifrados del otro lado, ¿puede estar seguro de que nadie ha cambiado los datos cifrados en el camino hacia usted?
Por lo general, la solución es calcular el resumen del mensaje a partir de los datos antes de cifrarlo, luego cifrar los datos y el resumen del mensaje, y enviarlo a través de la red. Un resumen de mensaje es un valor hash calculado en función de los datos del mensaje. Si se modifica al menos un byte en los datos cifrados, el resumen del mensaje calculado a partir de los datos también cambiará.
Cuando recibe datos cifrados, los descifra, calcula el resumen del mensaje a partir de ellos y compara el resumen calculado del mensaje con el resumen del mensaje enviado junto con los datos cifrados. Si los dos resúmenes de mensajes son iguales, existe una alta probabilidad (pero no del 100%) de que los datos no se hayan cambiado.
Java MessageDigest (java.security.MessageDigest) se puede utilizar para calcular resúmenes de mensajes. Para crear una instancia de MessageDigest, se llama al método MessageDigest.getInstance () . Hay varios algoritmos de resumen de mensajes diferentes. Debe especificar qué algoritmo desea usar al crear la instancia de MessageDigest. El trabajo con MessageDigest se describirá con más detalle en el capítulo Java MessageDigest.
Una breve introducción a la clase MessageDigest:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
Este ejemplo crea una instancia de MessageDigest que utiliza el algoritmo de cifrado interno criptográfico SHA-256 para calcular resúmenes de mensajes.
Para calcular el resumen del mensaje de algunos datos, llame al método update () o digest () . El método update () se puede llamar varias veces y el resumen del mensaje se actualiza dentro del objeto. Cuando haya pasado todos los datos que desea incluir en el resumen del mensaje, llame al resumen () y recupere el resumen del resumen del mensaje.
Un ejemplo de llamar a update () varias veces, seguido de una llamada a digest () :
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); byte[] data1 = "0123456789".getBytes("UTF-8"); byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); messageDigest.update(data1); messageDigest.update(data2); byte[] digest = messageDigest.digest();
También puede llamar a digest () una vez, pasando todos los datos para calcular el resumen del mensaje. Un ejemplo:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); byte[] data1 = "0123456789".getBytes("UTF-8"); byte[] digest = messageDigest.digest(data1);
Código de autenticación de mensaje (MAC)
La clase Java Mac se usa para crear un MAC (Código de autenticación de mensaje) a partir de un mensaje. El MAC es similar a un resumen del mensaje, pero utiliza una clave adicional para cifrar el resumen del mensaje. Solo teniendo los datos de origen y la clave, puede verificar el MAC. Por lo tanto, un MAC es una forma más segura de proteger un bloque de datos de modificaciones que un resumen de mensaje. La clase Mac se describe con más detalle en el capítulo Java Mac, seguido de una breve introducción.
Una instancia de Java Mac se crea llamando al método Mac.getInstance () , pasando el nombre del algoritmo para usar como parámetro. Así es como se ve:
Mac mac = Mac.getInstance("HmacSHA256");
Antes de crear un MAC a partir de datos, debe inicializar la instancia de Mac con la clave. Aquí hay un ejemplo de inicialización de una instancia de Mac con una clave:
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8 ,9,10,11,12,13,14,15}; String algorithm = "RawBytes"; SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm); mac.init(key);
Después de inicializar la instancia de Mac, puede calcular el MAC a partir de los datos llamando a los métodos update () y doFinal () . Si tiene todos los datos para calcular el MAC, puede llamar inmediatamente al método doFinal () . Así es como se ve:
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); byte[] data2 = "0123456789".getBytes("UTF-8"); mac.update(data); mac.update(data2); byte[] macBytes = mac.doFinal();
Firma
La clase Signature (java.security.Signature) se usa para firmar datos digitalmente. Cuando se firman los datos, se crea una firma digital a partir de estos datos. Por lo tanto, la firma se separa de los datos.
Se crea una firma digital creando un resumen de mensaje (hash) a partir de los datos y encriptando este resumen de mensaje con la clave privada del dispositivo, persona u organización que debe firmar los datos. El resumen del mensaje cifrado se denomina firma digital.
Para crear una instancia de Signature, se llama al método Signature.getInstance (...) :
Signature signature = Signature.getInstance("SHA256WithDSA");
Firma de datos
Para firmar datos, debe inicializar la instancia de firma en modo firma llamando al método initSign (...), pasando la clave privada para firmar los datos. Un ejemplo de inicialización de una instancia de firma en modo de firma:
signature.initSign(keyPair.getPrivate(), secureRandom);
Después de inicializar la instancia de firma, se puede usar para firmar los datos. Esto se hace llamando al método update (), pasando los datos de la firma como un parámetro. Puede llamar al método update () varias veces para complementar los datos para crear la firma. Después de pasar todos los datos al método update (), se llama al método sign () para obtener una firma digital. Así es como se ve:
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); signature.update(data); byte[] digitalSignature = signature.sign();
Verificación de firma
Para verificar la firma, debe inicializar la instancia de firma en modo de verificación llamando al método initVerify (...) , pasando como parámetro la clave pública que se utiliza para verificar la firma. Un ejemplo de inicialización de una instancia de firma en modo de verificación se ve así:
Signature signature = Signature.getInstance("SHA256WithDSA"); signature.initVerify(keyPair.getPublic());
Después de la inicialización en modo de verificación, los datos firmados se transmiten al método update () . Una llamada al método verificar () devuelve verdadero o falso dependiendo de si la firma se puede verificar o no. Aquí está la verificación de firma:
byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); signature2.update(data2); boolean verified = signature2.verify(digitalSignature);
Firma completa y ejemplo de verificación
SecureRandom secureRandom = new SecureRandom(); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); KeyPair keyPair = keyPairGenerator.generateKeyPair(); Signature signature = Signature.getInstance("SHA256WithDSA"); signature.initSign(keyPair.getPrivate(), secureRandom); byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); signature.update(data); byte[] digitalSignature = signature.sign(); Signature signature2 = Signature.getInstance("SHA256WithDSA"); signature2.initVerify(keyPair.getPublic()); signature2.update(data); boolean verified = signature2.verify(digitalSignature); System.out.println("verified = " + verified);