Cifrado de Azure Blob Storage con Key Vault (client-side)

Azure Key Vault ademas de permitir contener llaves (public key) y secretos (secret keys), permite realizar procesos de encriptación / desencriptación de mensajes sin comprometer a nuestra aplicación de conocer la llave privada (pensando en un algoritmo asimétrico).

Idealmente deberíamos encriptar el contenido de todos los datos que guardamos sobre Azure Storage, esto lo podemos lograr muy facil con un contenedor de llaves con Azure Key Vault sin comprometer a los desarrolladores ni aplicaciones de conocer las llaves de encripción. Este proceso ya viene implementado cuando ocupamos el SDK de Azure Storage. Se establece en las políticas en el objeto BlobRequestOptions sobre la propiedad EncryptionPolicy.

Esto NO esta implementado aun para la libreria de Net Core hasta la fecha de publicación de esta entrada.

El algoritmo que se ocupa para encriptar el contenido sobre el contenedor de almacenamiento se conoce como digital envelope o envelope encryption. Este algoritmo lo implementa el SDK de Azure Storage, y lo implementan otros proveedores (AWS, Google Cloud, etc). Y es un algoritmo de encripción básico para poder compartir mensajes encriptados sin comprometer las llaves de encripción entre el emisor y receptor.

Vamos a realizar la implementación paso a paso en .NET Core con Azure Key Vault y Azure Storage.

 

Azure Key Vault & Azure Storage

 

Primero debemos tener listo nuestro contenedor de Key Vault y una cuenta de Azure Storage, puede ser la de desarrollo. Debemos tener también implementado nuestro método de autenticación implementado para Key Vault con un Azure AD. Para esto dejo mi entrada anterior donde se explica como se hace.

Blob Envelope Encryption

Antes de escribir código hay que entender como funciona el proceso de encripción:

Envelope Encryption Path

El diagrama muestra los pasos del algoritmo envelope encryption. Revisemos los pasos con su respectiva parte de código.

1 – Creación de llave

En este paso debemos de crear una llave en el contenedor de Key Vault, si ya tenemos la llave entonces la solicitamos por su identificador. Las llaves en Key Vault son llaves publicas (o llaves para algoritmos simétricos, hay varias opciones).

En el método CreateMasterKey creamos nuestra llave indicado el tipo (RSA) y el tamaño, y lo único que nos interesa recuperar es su identificador único, que es una URI compuesta por la url de nuestro contenedor, el nombre y un hash de la versión.

2 – Create Symetric Key

Necesitamos crear una llave simétrica, que es con la que en realidad vamos a encriptar el archivo, esto se debe hacer así porque encriptar con un algoritmo asimétrico lo hace muy lento. Teniendo la llave, la vamos a encriptar con la llave publica que creamos en el paso anterior, a esto se le conoce como Wrap Key.

En el metodo CreateEnvelopeEncripted ocupamos AesCryptoServiceProvider (podría ser cualquier otro)  que define todas las características de la llave del algoritmo simétrico que vamos a ocupar. Establecemos algunas cosas como el Mode y el Padding. Como parámetro recibe un objeto del tipo IKey, este objeto es el que trae la definición de una llave que en este caso proviene de Azure Key Vault. Ese objeto jwk es nuestra llave maestra que creamos en el primer paso.

En ese mismo método realizamos el proceso de WrapKey, que como ya lo habíamos dicho lo que hace es encriptar la llave simétrica.

Recuperamos todos los parámetros de los algoritmos que estamos aplicando y de la llave encriptada (Wrapped Key) y lo regresamos en un objeto del tipo EnvelopeEncripted.

3 – Wrap Symetric Key

Ya mencionamos un poco de esto en el paso anterior. Aquí como ya lo habíamos dicho se encripta la llave simétrica con la llave publica de nuestra llave maestra que creamos en el primer paso. El método que realiza esto es WrapKeyAsync, este método esta implementado como asíncrono pero en realidad ocurre de forma local, no se hace ninguna petición al contenedor de Key Vault. Esto es así porque el objeto jwk ya contiene la llave publica.

Es importante guardar este valor, porque se ocupara mas adelante. Nosotros lo estamos guardando sobre la propiedad SymetricKeyEcripted.

4 – Encrypt

Aqui encriptamos el contenido del archivo o del blob con la llave simetrica que creamos previamente. Simplemente realizamos la encripcion.

Enfoquémonos en el primer método InternalEnvelopeEncription, este método es el que invoca internamente a CreateEnvelopeEncripted para obtener la llave simétrica de encripcion. Este método también recibe la llave IKey y un array que contiene los datos a encriptar.

Ahora veamos el método EncriptEvelopeData que es el que en invoca a InternalEnvelopeEncription, este método recupera toda la metadata de la encripción, la serializa sobre JSON y crea un KeyValuePair colocando el valor de la serialización sobre una constante que es la llave para el objeto. Esto es porque esa metadata debe existir con el contenido del archivo encriptado, es ahí donde hace sentido la palabra envelope. Y eso es lo que devuelve sobre un objeto Tuple.

5 – Envelope

Este paso debe lograr contener el archivo encriptado y la llave simétrica encriptada (Wrapped Key). Hay varias alternativas para lograrlo. En nuestro caso vamos a utilizar la metadata que existe en los Blobs del Storage, esta metadata solo es un diccionario de llave-valor que vive con cada Blob.

Después de recuperar el contenido encriptado y toda la información de la llave encriptada y parámetros del algoritmo que se aplico, lo agregamos como metadata del Blob.

El parámetro UploadBlobInfo que recibe el método UploadBlob permite indicar si el contenido debe ir encriptado o no.

La única forma de averiguar la llave de encriptación es desencriptandola con la llave privada de la llave maestra. Adicional podríamos guardar un hash en la metadata para validar que no haya sido alterado el contenido, o que corresponda a la ultima versión.

6 – Upload

Este ultimo paso se refiere a persistir el archivo sobre algún contenedor en Azure Storage. Esto lo hacemos con el método UploadFromStreamAsync, internamente hace la carga del contenido y también la carga de los metadata. Es importante que estos metadata existan sobre el Blob porque se ocuparan para la desencripción.

Dentro del diagrama se establece un componente llamado client-side como quien lleva la orquestación de este proceso. Este componente puede ser cualquier cosa, puede ser nuestra solución ASP .NET Core, Console .NET Core, WebApi, WCF, etc.

Blob Envelope Decryption

Si tenemos proceso de encripción debemos de implementar el proceso de desencripción aplicando el mismo algoritmo.

Envelope Decryption Path

El diagrama muestra los pasos para desencriptar sobre el algoritmo envelope encryption. Revisemos los pasos con su respectiva parte de código.

1 – Download Blob

Primero debemos de obtener el Blob desde el contenedor donde lo guardamos. Debemos descargarlo como normalmente lo haríamos con cualquier archivo, ya sea que este encriptado o no.

El método DownloadBlob recibe como parámetro el contenedor y la ruta del archivo. Lo descargamos y después validamos si es un Blob que esta encriptado, esto lo hacemos revisando si en el diccionario de metadata existe a llave con la que establecimos los parámetros de encripción.

Si esta encriptado invocamos el método DecriptEnvelopeData.

2 – Envelope

El Blob que descargamos contiene el archivo encriptado y la llave encriptada. Para poder realizar la desencripción debemos primero desencriptar la llave, este proceso se llama Unwrap Key. Y lo hacemos con la llave maestra.

El método CreateEnvelopeEncriptedForDecription recibe como parámetro la metadata, esta metadata se encuentra serializada sobre JSON. La deserializamos y recuperamos toda la información del algoritmo con el que se encriptó, incluyendo la llave encriptada.

Adicional en la información de metadata tenemos el identificador de la llave que se encuentra en el contenedor de Azure Key Vault, debemos ir a recuperar esa llave. Esto permite tener distintas llaves para cada Blob, por eso es que guardamos el identificador sobre la metadata. Esto no compromete nada, el identificador de la llave no dice nada que vulnere la solución.

3 – Uwrap Symetric Key

Una vez que tenemos el valor de la llave encriptada debemos realizar la operación inversa, para obtener la llave simétrica de encripción de nuevo.

El método UnwrapKeyAsync como su nombre lo dice es el que realiza la descripción de la llave simétrica. Este proceso no se realiza de forma local, aquí si se hace una petición donde Key Vault se encarga de desencriptar con la llave privada de la llave maestra. Esto es bueno, porque no comprometemos la aplicación, ya que no exponemos la llave privada.

4 – Symetric Key

Este paso solamente se refiere a que ya tenemos nuestra llave lista. Ya la podemos ocupar para realizar la desencripcion de lo que encriptamos previamente.

5 – Decrypt File

Finalmente ya que tenemos la llave realizamos la desencripción del archivo.

Realizamos la desencripción del contenido y lo devolvemos sobre la propiedad Data, que se refiere al contenido sin encriptar.

Dejo también la definición de la clase EnvelopeEncripted:

Es importante cuidar que en la serialización NUNCA se agregue la propiedad SymetricKey.

Client-Side

Este proceso de encripción sobre el Storage se realiza de forma controlada sobre nuestra solución que puede ser: ASP .NET Core, Console .NET Core o cualquier otra. Esto también se puede lograr haciéndolo desde el portal de Azure y activarlo desde el servicio de Storage, pero no se tiene control de como se hace.

En nuestro caso hicimos la implementación sobre una solución de consola:

Toda la lógica se encuentra sobre la clase EnvelopeCryptoStorageEngine, que tiene dependencia sobre una cuenta de Sotrage y un KeyResolver (para Azure Key Vault).

Dejo el enlace de github para ver la solución completa aquí.