Cifrado Híbrido con Clave de Sesión en Go
El cifrado es un tema interesante y complejo, incluso crucial en el desarrollo moderno de aplicaciones, especialmente cuando manejamos datos sensibles que necesitan protección tanto en tránsito como en almacenamiento. Uno de los métodos de cifrado más efectivos y seguros es el cifrado híbrido con claves de sesión. Este artículo está basado en un problema reciente que tuve, que me llevó a investigar y llegar a esta solución. Vamos a explorar cómo funciona el cifrado híbrido, por qué es necesario y cómo implementarlo en Go.
El Problema: Cifrado Solo con RSA
RSA es un algoritmo de cifrado asimétrico muy utilizado, pero está limitado en la cantidad de datos que puede cifrar. Esta limitación proviene del tamaño de la clave RSA y del esquema de relleno que se usa para la seguridad. Por ejemplo, si utilizamos una clave de 2048 bits con relleno OAEP, el tamaño máximo de datos que podemos cifrar es de aproximadamente 245 bytes. Esto significa que no podemos cifrar directamente grandes volúmenes de datos, como documentos o mensajes extensos, utilizando solo RSA.
// Ejemplo de cifrado con RSA
func EncryptWithPublicKey(data []byte, publicKeyPem string) ([]byte, error) {
// 1. Decodificar la clave pública en formato PEM
block, _ := pem.Decode([]byte(publicKeyPem))
if block == nil || block.Type != "PUBLIC KEY" {
return nil, errors.New("failed to decode PEM block containing public key")
}
// 2. Parsear la clave pública RSA
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse public key: %v", err)
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, errors.New("not an RSA public key")
}
// 3. Cifrar los datos usando OAEP con SHA-256
hash := sha256.New()
encryptedData, err := rsa.EncryptOAEP(hash, rand.Reader, rsaPub, data, nil)
if err != nil {
return nil, fmt.Errorf("error encrypting data: %v", err)
}
return encryptedData, nil
}
Este método de cifrado es ideal para datos pequeños. Sin embargo, para datos grandes, necesitamos un método que permita cifrar volúmenes de datos de cualquier tamaño sin perder seguridad ni eficiencia.
La Solución: Cifrado Híbrido con Clave de Sesión
El cifrado híbrido combina cifrado simétrico y asimétrico para aprovechar los beneficios de ambos métodos. Aquí tienes los pasos:
- Generación de Clave de Sesión (AES): Se genera una clave de sesión aleatoria (AES-256) para cifrar los datos completos.
- Cifrado de la Clave de Sesión con RSA: Luego, esta clave de sesión se cifra con RSA utilizando la clave pública del destinatario. Esto permite que solo el destinatario pueda recuperar la clave de sesión y, por tanto, descifrar el mensaje.
Cifrado con Clave de Sesión (Encriptar)
func EncryptWithPublicKey(data []byte, publicKeyPem string) ([]byte, error) {
// 1. Decodificar la clave pública
block, _ := pem.Decode([]byte(publicKeyPem))
if block == nil || block.Type != "PUBLIC KEY" {
return nil, errors.New("failed to decode PEM block containing public key")
}
// 2. Parsear la clave pública RSA
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse public key: %v", err)
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, errors.New("not an RSA public key")
}
// 3. Generar clave de sesión AES
aesKey := make([]byte, 32) // 256 bits para AES-256
if _, err := rand.Read(aesKey); err != nil {
return nil, fmt.Errorf("failed to generate AES key: %v", err)
}
// 4. Cifrar datos con AES-GCM
blockCipher, err := aes.NewCipher(aesKey)
if err != nil {
return nil, fmt.Errorf("failed to create AES cipher: %v", err)
}
gcm, err := cipher.NewGCM(blockCipher)
if err != nil {
return nil, fmt.Errorf("failed to create AES-GCM cipher: %v", err)
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, fmt.Errorf("failed to generate nonce: %v", err)
}
// 5. Cifrar el contenido usando AES y el nonce
encryptedData := gcm.Seal(nonce, nonce, data, nil)
// 6. Cifrar la clave de sesión AES con RSA
hash := sha256.New()
encryptedAESKey, err := rsa.EncryptOAEP(hash, rand.Reader, rsaPub, aesKey, nil)
if err != nil {
return nil, fmt.Errorf("error encrypting AES key with RSA: %v", err)
}
// 7. Combinar clave de sesión cifrada y contenido cifrado
finalEncryptedData := append(encryptedAESKey, encryptedData...)
return finalEncryptedData, nil
}
Descifrado con Clave de Sesión (Desencriptar)
El descifrado sigue el proceso inverso: primero desciframos la clave de sesión usando RSA y luego desciframos el contenido con AES.
func DecryptWithPrivateKey(encryptedData []byte, encryptedPrivateKeyBase64 string, iv []byte, passphrase string) ([]byte, error) {
// 1. Decodificar y descifrar la clave privada
encryptedPrivateKeyBytes, err := base64.StdEncoding.DecodeString(encryptedPrivateKeyBase64)
if err != nil {
return nil, fmt.Errorf("failed to decode base64 private key: %v", err)
}
privateKeyPem, err := DecryptAES256GCM(encryptedPrivateKeyBytes, iv, passphrase)
if err != nil {
return nil, fmt.Errorf("failed to decrypt private key: %v", err)
}
block, _ := pem.Decode(privateKeyPem)
if block == nil {
return nil, errors.New("failed to decode PEM block containing private key")
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %v", err)
}
// 2. Separar clave de sesión y contenido cifrado
rsaKeySize := privateKey.Size()
encryptedAESKey := encryptedData[:rsaKeySize]
encryptedContent := encryptedData[rsaKeySize:]
// 3. Descifrar la clave de sesión con RSA
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, encryptedAESKey, nil)
if err != nil {
return nil, fmt.Errorf("error decrypting AES key: %v", err)
}
// 4. Crear bloque de cifrado AES y descifrar el contenido
blockCipher, err := aes.NewCipher(aesKey)
if err != nil {
return nil, fmt.Errorf("failed to create AES cipher: %v", err)
}
gcm, err := cipher.NewGCM(blockCipher)
if err != nil {
return nil, fmt.Errorf("failed to create AES-GCM cipher: %v", err)
}
// 5. Extraer el nonce y descifrar el contenido
nonceSize := gcm.NonceSize()
nonce, encryptedContentData := encryptedContent[:nonceSize], encryptedContent[nonceSize:]
decryptedData, err := gcm.Open(nil, nonce, encryptedContentData, nil)
if err != nil {
return nil, fmt.Errorf("error decrypting data with AES: %v", err)
}
return decryptedData, nil
}
Beneficios del Cifrado Híbrido con Clave de Sesión
- Flexibilidad para Datos Grandes: Al usar AES para el contenido, podemos cifrar datos de cualquier tamaño.
- Seguridad: AES-GCM garantiza tanto la confidencialidad como la integridad de los datos.
- Eficiencia: AES es más rápido que RSA, lo que mejora el rendimiento en volúmenes de datos grandes.
Limitaciones y Desafíos
- Complejidad: Requiere una implementación avanzada y cuidadosa.
- Manejo de Claves: Es necesario gestionar la clave de sesión y las claves RSA, lo que implica consideraciones adicionales de seguridad.
- Compatibilidad: Los datos cifrados previamente solo con RSA deben migrarse o se necesita mantener múltiples versiones de descifrado.
Conclusión
El cifrado híbrido con clave de sesión es una solución poderosa para manejar grandes volúmenes de datos cifrados de manera segura y eficiente sin la limitacion del tamano de la data. Aunque introduce cierta complejidad, sus beneficios en flexibilidad y seguridad hacen que valga la pena la implementación en proyectos que requieran protección robusta de datos.
Espero que este artículo te haya dado una comprensión sólida del cifrado híbrido con claves de sesión y te anime a implementarlo en tus propios proyectos.
Written with StackEdit.