Almacenamiento de datos de forma segura en Android

La credibilidad de una aplicación hoy depende en gran medida de cómo se administran los datos privados del usuario. La pila de Android tiene muchas API potentes que rodean las credenciales y el almacenamiento de claves, con características específicas que solo están disponibles en ciertas versiones. 

Esta breve serie comenzará con un enfoque simple para comenzar a utilizar el sistema de almacenamiento y cómo cifrar y almacenar datos confidenciales a través de un código de acceso proporcionado por el usuario. En el segundo tutorial, veremos formas más complejas de proteger claves y credenciales.

Los basicos

La primera pregunta a considerar es la cantidad de datos que realmente necesita adquirir. Un buen enfoque es evitar el almacenamiento de datos privados si realmente no tiene que hacerlo..

Para los datos que debe almacenar, la arquitectura de Android está lista para ayudar. Desde la versión 6.0 Marshmallow, el cifrado de disco completo está habilitado de forma predeterminada, para dispositivos con la capacidad. Archivos y Preferencias compartidas que son guardados por la aplicación se configuran automáticamente con el MODE_PRIVATE constante. Esto significa que solo se puede acceder a los datos mediante su propia aplicación. 

Es una buena idea atenerse a este valor predeterminado. Puede configurarlo explícitamente al guardar una preferencia compartida.

SharedPreferences.Editor editor = getSharedPreferences ("preferencesName", MODE_PRIVATE) .edit (); editor.putString ("clave", "valor"); editor.commit ();

O al guardar un archivo.

FileOutputStream fos = openFileOutput (filenameString, Context.MODE_PRIVATE); fos.write (datos); fos.close ();

Evite almacenar datos en un almacenamiento externo, ya que otros usuarios y aplicaciones pueden ver los datos. De hecho, para que a las personas les resulte más difícil copiar los datos y binarios de su aplicación, puede evitar que los usuarios puedan instalar la aplicación en un almacenamiento externo. Añadiendo Android: instalar ubicación con un valor de internoOnly para el archivo de manifiesto logrará que.

También puede evitar que se realice una copia de seguridad de la aplicación y sus datos. Esto también evita que los contenidos del directorio de datos privados de una aplicación se descarguen usando copia de seguridad adb. Para ello, establece el Android: allowBackup atribuir a falso en el archivo manifiesto. De forma predeterminada, este atributo se establece en cierto.

Estas son las mejores prácticas, pero no funcionarán para un dispositivo comprometido o rooteado, y el cifrado del disco solo es útil cuando el dispositivo está protegido con una pantalla de bloqueo. Aquí es donde es beneficioso tener una contraseña del lado de la aplicación que proteja sus datos con cifrado.

Asegurar los datos del usuario con una contraseña

Conceal es una excelente opción para una biblioteca de encriptación porque lo pone en funcionamiento muy rápidamente sin tener que preocuparse por los detalles subyacentes. Sin embargo, un exploit dirigido a un marco popular afectará simultáneamente todas las aplicaciones que dependen de él. 

También es importante tener conocimiento sobre cómo funcionan los sistemas de encriptación para poder saber si está utilizando un marco en particular de forma segura. Por lo tanto, para esta publicación, nos ensuciaremos las manos al consultar directamente al proveedor de criptografía. 

Derivación de claves basadas en contraseña y AES

Utilizaremos el estándar AES recomendado, que cifra los datos a partir de una clave. La misma clave utilizada para cifrar los datos se utiliza para descifrar los datos, lo que se denomina cifrado simétrico. Existen diferentes tamaños de clave, y AES256 (256 bits) es la longitud preferida para usar con datos confidenciales.

Si bien la experiencia de usuario de su aplicación debería obligar a un usuario a usar un código de acceso seguro, existe la posibilidad de que otro usuario también elija el mismo código de acceso. Poner la seguridad de nuestros datos cifrados en manos del usuario no es seguro. Nuestros datos necesitan ser protegidos en lugar de un llave eso es aleatorio y lo suficientemente grande (es decir, que tiene suficiente entropía) para ser considerado fuerte. Es por esto que nunca se recomienda usar una contraseña directamente para cifrar los datos, es decir, donde se llama una función Función de derivación de clave basada en contraseña (PBKDF2) entra en juego. 

PBKDF2 deriva un llave a partir de una contraseña por picadillo muchas veces con sal. Esto se llama estiramiento clave. La sal es solo una secuencia aleatoria de datos y hace que la clave derivada sea única incluso si otra persona usó la misma contraseña. 

Empecemos por generar esa sal.. 

SecureRandom random = new SecureRandom (); byte salt [] = nuevo byte [256]; random.nextBytes (sal);

los SecureRandom la clase garantiza que la salida generada será difícil de predecir, es un "generador de números aleatorios criptográficamente sólido". Ahora podemos poner el salt y la contraseña en un objeto de cifrado basado en contraseña: PBEKeySpec. El constructor del objeto también toma una forma de recuento de iteraciones, haciendo que la clave sea más fuerte. Esto se debe a que al aumentar el número de iteraciones se expande el tiempo que llevaría operar un conjunto de teclas durante un ataque de fuerza bruta. los PBEKeySpec luego pasa a la SecretKeyFactory, que finalmente genera la clave como byte[] formación. Vamos a envolver eso crudo byte[] matriz en un SecretKeySpec objeto.

char [] passwordChar = passwordString.toCharArray (); // Convertir la contraseña en char [] array PBEKeySpec pbKeySpec = new PBEKeySpec (passwordChar, salt, 1324, 256); // 1324 iteraciones SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = new SecretKeySpec (keyBytes, "AES");

Tenga en cuenta que la contraseña se pasa como carbonizarse[] matriz, y la PBEKeySpec clase lo almacena como una carbonizarse[] matriz también. carbonizarse[] Los arreglos se usan generalmente para funciones de encriptación porque mientras que Cuerda la clase es inmutable, una carbonizarse[] La matriz que contiene información confidencial se puede sobrescribir, eliminando así los datos confidenciales de la memoria del dispositivo..

Vectores de inicializacion

Ahora estamos listos para cifrar los datos, pero tenemos una cosa más que hacer. Hay diferentes modos de cifrado con AES, pero usaremos el recomendado: el cifrado de bloque de cadena (CBC). Esto opera en nuestros datos un bloque a la vez. Lo mejor de este modo es que cada siguiente bloque de datos sin cifrar está en XOR con el bloque cifrado anterior para hacer que el cifrado sea más sólido. Sin embargo, eso significa que el primer bloque nunca es tan único como todos los demás.! 

Si un mensaje a cifrar comenzara de la misma manera que otro mensaje a cifrar, la salida encriptada inicial sería la misma, y ​​eso le daría al atacante una pista para averiguar cuál podría ser el mensaje. La solución es utilizar un vector de inicialización (IV).. 

Un IV es solo un bloque de bytes aleatorios que serán XOR con el primer bloque de datos del usuario. Dado que cada bloque depende de todos los bloques procesados ​​hasta ese momento, todo el mensaje se cifrará de manera única, los mensajes idénticos cifrados con la misma clave no producirán resultados idénticos. 

Vamos a crear una IV ahora.

SecureRandom ivRandom = new SecureRandom (); // no se almacena en caché la instancia inicial anterior del byte SecureRandom [] iv = new byte [16]; ivRandom.nextBytes (iv); IvParameterSpec ivSpec = new IvParameterSpec (iv);

Una nota sobre SecureRandom. En las versiones 4.3 y anteriores, la arquitectura de criptografía de Java tenía una vulnerabilidad debido a la inicialización incorrecta del generador de números pseudoaleatorios subyacente (PRNG). Si está apuntando a versiones 4.3 y menores, hay una solución disponible.

Encriptar los datos

Armado con un IvParameterSpec, Ahora podemos hacer el cifrado real.

Cipher cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); cipher.init (Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte [] encrypted = cipher.doFinal (plainTextBytes);

Aquí pasamos en la cadena. "AES / CBC / PKCS7Padding". Esto especifica el cifrado AES con encadenamiento de bloques cifrados. La última parte de esta cadena se refiere a PKCS7, que es un estándar establecido para datos de relleno que no encajan perfectamente en el tamaño del bloque. (Los bloques son de 128 bits y el relleno se realiza antes del cifrado).

Para completar nuestro ejemplo, pondremos este código en un método de cifrado que empaquetará el resultado en un HashMap que contiene los datos cifrados, junto con el vector de sal y de inicialización necesarios para el descifrado.

HashMap privado encryptBytes (byte [] plainTextBytes, String passwordString) HashMap map = new HashMap(); intente // sal aleatoria para el siguiente paso SecureRandom random = new SecureRandom (); byte salt [] = nuevo byte [256]; random.nextBytes (sal); // PBKDF2 - deriva la clave de la contraseña, no use las contraseñas directamente char [] passwordChar = passwordString.toCharArray (); // Convertir la contraseña en char [] array PBEKeySpec pbKeySpec = new PBEKeySpec (passwordChar, salt, 1324, 256); // 1324 iteraciones SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = new SecretKeySpec (keyBytes, "AES"); // Crear el vector de inicialización para AES SecureRandom ivRandom = new SecureRandom (); // no se almacena en caché la instancia inicial anterior del byte SecureRandom [] iv = new byte [16]; ivRandom.nextBytes (iv); IvParameterSpec ivSpec = new IvParameterSpec (iv); // Encrypt Cipher cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); cipher.init (Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte [] encrypted = cipher.doFinal (plainTextBytes); map.put ("sal", sal); map.put ("iv", iv); map.put ("encriptado", encriptado);  catch (Exception e) Log.e ("MYAPP", "excepción de cifrado", e); mapa de retorno; 

El método de descifrado

Solo necesitas almacenar la IV y la sal con tus datos. Si bien las sales y las IV se consideran públicas, asegúrese de que no se incrementen o reutilicen secuencialmente. Para descifrar los datos, todo lo que tenemos que hacer es cambiar el modo en el Cifrar constructor de ENCRYPT_MODE a DECRYPT_MODE

El método de descifrado tendrá una HashMap que contiene la misma información requerida (datos cifrados, sal y IV) y devolver un descifrado byte[] matriz, dada la contraseña correcta. El método de descifrado regenerará la clave de cifrado a partir de la contraseña. La llave nunca debe ser almacenada!

byte privado [] decryptData (HashMap map, String passwordString) byte [] decrypted = null; intente byte salt [] = map.get ("salt"); byte iv [] = map.get ("iv"); byte encriptado [] = map.get ("encriptado"); // regenerar clave desde la contraseña char [] passwordChar = passwordString.toCharArray (); PBEKeySpec pbKeySpec = new PBEKeySpec (passwordChar, salt, 1324, 256); SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = new SecretKeySpec (keyBytes, "AES"); // Descifrar cifrado cifrado = Cipher.getInstance ("AES / CBC / PKCS7Padding"); IvParameterSpec ivSpec = new IvParameterSpec (iv); cipher.init (Cipher.DECRYPT_MODE, keySpec, ivSpec); descifrado = cipher.doFinal (encriptado);  catch (Exception e) Log.e ("MYAPP", "excepción de descifrado", e);  retorno descifrado; 

Probando el cifrado y descifrado

Para mantener el ejemplo simple, estamos omitiendo la verificación de errores que aseguraría que HashMap Contiene la clave requerida, pares de valores. Ahora podemos probar nuestros métodos para asegurarnos de que los datos se descifran correctamente después del cifrado.

// Cadena de prueba de cifrado cadena = "Mi cadena sensible que quiero cifrar"; byte [] bytes = string.getBytes (); HashMap map = encryptBytes (bytes, "UserSuppliedPassword"); // Byte de prueba de descifrado [] decrypted = decryptData (map, "UserSuppliedPassword"); if (descifrado! = nulo) String decryptedString = new String (descifrado); Log.e ("MYAPP", "Cadena desencriptada es:" + decryptedString); 

Los métodos utilizan un byte[] matriz para que pueda cifrar datos arbitrarios en lugar de solo Cuerda objetos. 

Guardar datos cifrados

Ahora que tenemos un cifrado byte[] Array, podemos guardarlo en el almacenamiento..

FileOutputStream fos = openFileOutput ("test.dat", Context.MODE_PRIVATE); fos.write (encriptado); fos.close ();

Si no quieres guardar la IV y la sal por separado., HashMap es serializable con el ObjectInputStream y ObjectOutputStream clases.

FileOutputStream fos = openFileOutput ("map.dat", Context.MODE_PRIVATE); ObjectOutputStream oos = new ObjectOutputStream (fos); oos.writeObject (mapa); oos.close ();

Guardar datos seguros en Preferencias compartidas

También puede guardar datos seguros en su aplicación Preferencias compartidas.

SharedPreferences.Editor editor = getSharedPreferences ("prefs", Context.MODE_PRIVATE) .edit (); String keyBase64String = Base64.encodeToString (encryptedKey, Base64.NO_WRAP); String valueBase64String = Base64.encodeToString (encryptedValue, Base64.NO_WRAP); editor.putString (keyBase64String, valueBase64String); editor.commit ();

Desde el Preferencias compartidas es un sistema XML que solo acepta primitivos específicos y objetos como valores, necesitamos convertir nuestros datos a un formato compatible como un Cuerda objeto. Base64 nos permite convertir los datos en bruto en un Cuerda Representación que contiene solo los caracteres permitidos por el formato XML. Cifre tanto la clave como el valor para que un atacante no pueda entender para qué sirve un valor. 

En el ejemplo anterior, encryptedKey y encryptedValue ambos están encriptados byte[] matrices regresadas de nuestro encryptBytes () método. La IV y la sal se pueden guardar en el archivo de preferencias o como un archivo separado. Para recuperar los bytes encriptados de la Preferencias compartidas, Podemos aplicar una decodificación Base64 en el almacenado. Cuerda.

SharedPreferences preferences = getSharedPreferences ("prefs", Context.MODE_PRIVATE); String base64EncryptedString = preferences.getString (keyBase64String, "default"); byte [] encryptedBytes = Base64.decode (base64EncryptedString, Base64.NO_WRAP);

Borrar datos inseguros de versiones anteriores

Ahora que los datos almacenados están seguros, puede darse el caso de que tenga una versión anterior de la aplicación que tenía los datos almacenados de forma insegura. En una actualización, los datos se pueden borrar y volver a cifrar. El siguiente código borra un archivo usando datos aleatorios. 

En teoría, solo puede eliminar sus preferencias compartidas eliminando /data/data/com.your.package.name/shared_prefs/your_prefs_name.xml y tu_prefs_name.bak archivos y borrar las preferencias en memoria con el siguiente código:

getSharedPreferences ("prefs", Context.MODE_PRIVATE) .edit (). clear (). commit ();

Sin embargo, en lugar de intentar borrar los datos antiguos y esperar que funcionen, ¡es mejor cifrarlos en primer lugar! Esto es especialmente cierto en general para unidades de estado sólido que a menudo extienden escrituras de datos a diferentes regiones para evitar el desgaste. Eso significa que incluso si sobrescribe un archivo en el sistema de archivos, la memoria física de estado sólido puede conservar sus datos en su ubicación original en el disco.

public static void secureWipeFile (Archivo) lanza IOException if (file! = null && file.exists ()) final long length = file.length (); final SecureRandom random = new SecureRandom (); RandomAccessFile final randomAccessFile = new RandomAccessFile (archivo, "rws"); randomAccessFile.seek (0); randomAccessFile.getFilePointer (); byte [] data = new byte [64]; posición int = 0; mientras < length)  random.nextBytes(data); randomAccessFile.write(data); position += data.length;  randomAccessFile.close(); file.delete();  

Conclusión

Eso envuelve nuestro tutorial sobre el almacenamiento de datos encriptados. En esta publicación, aprendió cómo cifrar y descifrar de forma segura los datos confidenciales con una contraseña proporcionada por el usuario. Es fácil de hacer cuando sabe cómo hacerlo, pero es importante seguir todas las mejores prácticas para garantizar que los datos de sus usuarios sean realmente seguros..

En la próxima publicación, veremos cómo aprovechar el KeyStore y otras API relacionadas con credenciales para almacenar elementos de forma segura. Mientras tanto, echa un vistazo a algunos de nuestros otros excelentes artículos sobre el desarrollo de aplicaciones para Android..