Spanish (Español) translation by James (you can also view the original English article)
En el post anterior sobre la seguridad de datos de usuario de Android, miramos en el cifrado de datos a través de un código de acceso suministrado por el usuario. Este tutorial desplazará el foco para credencial y almacenamiento de claves. Te comenzar introduciendo credenciales de la cuenta y terminar con un ejemplo de protección de datos utilizando la KeyStore.
- SeguridadAlmacenar Datos de Forma Segura en AndroidCollin Stuart
- AndroidCómo Proteger una Aplicación AndroidAshraff Hathibelagal
A menudo, cuando se trabaja con un servicio de terceros, habrá alguna forma de autenticación requerido. Esto puede ser tan simple como un extremo /login
que acepta un nombre de usuario y contraseña.
Al principio parece que una solución simple es crear una interfaz de usuario que solicita al usuario iniciar sesión y capturar y almacenar sus credenciales de inicio de sesión. Sin embargo, esto no es lo mejor porque nuestra aplicación no necesita saber las credenciales de una cuenta de terceros. En cambio, podemos utilizar el administrador de la cuenta, que delega el manejo de esa información sensible para nosotros.
Gerente de Cuentas
El administrador de cuentas es un ayudante centralizado para las credenciales de cuenta de usuario para que su aplicación no tiene que tratar directamente con las contraseñas. A menudo proporciona un token en lugar del verdadero usuario y contraseña que puede utilizar para hacer peticiones autenticadas a un servicio. Un ejemplo es cuando se solicita un token que usan OAuth2.
A veces, toda la información necesaria ya está almacenada en el dispositivo y otras veces el administrador de cuentas tendrá que llamar a un servidor para una muestra fresca. Usted pudo haber visto la sección de Cuentas en la configuración de su dispositivo para varias aplicaciones. Podemos obtener ese listado de cuentas disponibles como este:
AccountManager accountManager = AccountManager.get(this); Account[] accounts = accountManager.getAccounts();
El código requerirá el permiso de android.permission.GET_ACCOUNTS
. Si usted está buscando una cuenta específica, usted puede encontrar como esta:
AccountManager accountManager = AccountManager.get(this); Account[] accounts = accountManager.getAccountsByType("com.google");
Una vez tengas la cuenta, un símbolo (token) de la cuenta se puede recuperar llamando al método getAuthToken(Account, String, Bundle, Activity, AccountManagerCallback, Handler)
. El token puede utilizarse entonces para hacer solicitudes de API autenticadas a un servicio. Esto podría ser una API RESTful donde pasa en un parámetro token durante una solicitud HTTPS, sin tener que saber los datos del usuario cuenta privada.
Porque cada servicio tendrá una forma diferente de autenticación y el almacenamiento de las credenciales privadas, el administrador de cuentas proporciona módulos de autenticador a un servicio de terceros para implementar. Mientras que Android tiene implementaciones para muchos servicios populares, significa que usted puede escribir su propio autenticador para manejar el almacenamiento de autenticación y credenciales de cuenta de su aplicación. Esto le permite asegurarse de que las credenciales están encriptadas. Tenga en cuenta, esto también significa que las credenciales en el administrador de cuentas que son utilizadas por otros servicios se pueden almacenar en texto claro, haciéndolos accesibles a cualquier persona que se ha arraigado a su dispositivo.
En lugar de sencillas credenciales, hay veces cuando usted tendrá que lidiar con una clave o un certificado para un individuo o entidad, por ejemplo, cuando un tercero le envía un archivo de certificado que necesita mantener. El escenario más común es cuando una aplicación necesita para autenticar al servidor de una organización privada.
En el siguiente tutorial, buscará en el uso de certificados para la autenticación y comunicaciones seguras, pero todavía quiero abordar cómo almacenar estos elementos mientras tanto. La API de llavero fue construida originalmente para eso uso muy específico, instalar un par de certificado o clave privado de un archivo PKCS #12.
El Llavero
Introducido en Android 4.0 (API nivel 14), la API de llavero aborda la gestión de claves. Específicamente, se trabaja con objetos PrivateKey
y X509Certificate
y proporciona un contenedor más seguro que el uso de almacenamiento de datos de la aplicación. Eso es porque los permisos para las claves privadas permiten sólo para su propia aplicación de las claves de acceso y sólo después de la autorización de usuario. Esto significa que una pantalla de bloqueo debe configurar en el dispositivo antes de que usted puede hacer uso del almacenamiento de credenciales. También, los objetos en el llavero pueden obligarse al hardware seguro, si está disponible.
El código para instalar un certificado es como sigue:
Intent intent = KeyChain.createInstallIntent(); byte[] p12Bytes = //... read from file, such as example.pfx or example.p12... intent.putExtra(KeyChain.EXTRA_PKCS12, p12Bytes); startActivity(intent);
El usuario se solicitará una contraseña para acceder a la clave privada y una opción para el nombre del certificado. Para recuperar la clave, el siguiente código presenta una interfaz de usuario que permite al usuario elegir en la lista de teclas instaladas.
KeyChain.choosePrivateKeyAlias(this, this, new String[]{"RSA"}, null, null, -1, null);
Una vez hecha la elección, un nombre de alias de la cadena se devuelve en la devolución de llamada de alias(final String alias)
donde se puede acceder directamente la cadena de certificado o clave privada.
public class KeychainTest extends Activity implements ..., KeyChainAliasCallback { //... @Override public void alias(final String alias) { Log.e("MyApp", "Alias is " + alias); try { PrivateKey privateKey = KeyChain.getPrivateKey(this, alias); X509Certificate[] certificateChain = KeyChain.getCertificateChain(this, alias); } catch ... } //... }
Armados con ese conocimiento, ahora veamos cómo podemos utilizar el almacenamiento de credenciales para guardar datos confidenciales.
La KeyStore
En el tutorial anterior, vimos a la protección de datos a través de un código de acceso suministrado por el usuario. Este tipo de configuración es buena, pero los requisitos de la aplicación a menudo dirigen lejos de tener los usuarios iniciar sesión en cada momento y recordar una contraseña adicional.
Es donde se puede utilizar la API del almacén de claves. Desde entonces 1 de API, el almacén de claves se ha utilizado por el sistema para almacenar las credenciales VPN y WiFi. A partir de 4.3 (API 18), le permite trabajar con sus propias llaves asimétricas de aplicación específica, y en Android (API 23) puede almacenar una clave simétrica de AES. Así que mientras que el API no permite almacenar cuerdas sensibles directamente, estas teclas pueden almacenarse y luego utilizadas para cifrar cadenas.
El beneficio para almacenar una clave en el almacén de claves es que permite a las teclas para ser operados sin exponer el contenido secreto de esa clave; datos clave no entra en el espacio de aplicación. Recuerde que las claves están protegidas por permisos para que sólo su aplicación puede acceder a ellos, y pueden además ser seguros respaldado por el hardware Si el dispositivo es capaz. Esto crea un contenedor que hace más difícil extraer las claves de un dispositivo.
Generar una Nueva Clave al Azar
Para este ejemplo, en lugar de generar una clave de AES de un código de acceso suministrado por el usuario, nosotros podemos generar una clave aleatoria que se protegerán en el almacén de claves. Podemos hacer esto creando una instancia del KeyGenerator
, establecida en el proveedor de "AndroidKeyStore"
.
//Generate a key and store it in the KeyStore final KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder("MyKeyAlias", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) //.setUserAuthenticationRequired(true) //requires lock screen, invalidated if lock screen is disabled //.setUserAuthenticationValidityDurationSeconds(120) //only available x seconds from password authentication. -1 requires finger print - every time .setRandomizedEncryptionRequired(true) //different ciphertext for same plaintext on each call .build(); keyGenerator.init(keyGenParameterSpec); keyGenerator.generateKey();
Partes importantes que mirar aquí están las especificaciones de .setUserAuthenticationRequired(true)
y .setUserAuthenticationValidityDurationSeconds(120)
. Estos requieren una pantalla de bloqueo para configurar y la clave para bloquearse hasta que el usuario se ha autenticado.
Mirando la documentación de .setUserAuthenticationValidityDurationSeconds()
, verá que significa la clave sólo está disponible un cierto número de segundos de autenticación de contraseña y eso paso en -1 requiere la autenticación de huellas dactilares cada vez que quiera acceder a la clave. Habilitar el requisito de autenticación también tiene el efecto de revocar la clave cuando el usuario elimina o cambia la pantalla de bloqueo.
Porque almacenar una clave sin protección junto a los datos cifrados es como poner una llave de la casa bajo el felpudo, estas opciones intentan proteger la llave en reposo en caso de que un dispositivo está comprometido. Un ejemplo podría ser una descarga de datos sin conexión del dispositivo. Sin la contraseña de ser conocida por el dispositivo, que los datos se hace inútil.
La opción .setRandomizedEncryptionRequired(true)
permite el requisito de que hay suficiente aleatorización (una nueva al azar IV cada vez) para que si los mismos datos se cifraron una segunda vez, esa salida cifrada todavía será diferente. Esto evita que un atacante obtener pistas sobre el texto cifrado basado en la alimentación de los mismos datos.
Otra opción a destacar es setUserAuthenticationValidWhileOnBody(boolean remainsValid)
, que bloquea la clave una vez que el dispositivo ha detectado ya no está en la persona.
Cifrar Datos
Ahora que la clave se almacena en el almacén de claves, podemos crear un método que cifra los datos mediante el objeto de cifrado, dado el SecretKey. Devuelve un HashMap
que contiene los datos cifrados y un IV aleatorio que será necesario para descifrar los datos. Los datos cifrados, junto con el IV, pueden guardarse en un archivo o en las preferencias compartidas.
private HashMap<String, byte[]> encrypt(final byte[] decryptedBytes) { final HashMap<String, byte[]> map = new HashMap<String, byte[]>(); try { //Get the key final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry("MyKeyAlias", null); final SecretKey secretKey = secretKeyEntry.getSecretKey(); //Encrypt data final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, secretKey); final byte[] ivBytes = cipher.getIV(); final byte[] encryptedBytes = cipher.doFinal(decryptedBytes); map.put("iv", ivBytes); map.put("encrypted", encryptedBytes); } catch (Throwable e) { e.printStackTrace(); } return map; }
Descifrar a una Matriz de Bytes
Para descifrar, se aplica lo contrario. El objeto de Cipher
se inicializa utilizando la constante DECRYPT_MODE
, y se devuelve una matriz de byte[]
descifrados.
private byte[] decrypt(final HashMap<String, byte[]> map) { byte[] decryptedBytes = null; try { //Get the key final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry("MyKeyAlias", null); final SecretKey secretKey = secretKeyEntry.getSecretKey(); //Extract info from map final byte[] encryptedBytes = map.get("encrypted"); final byte[] ivBytes = map.get("iv"); //Decrypt data final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); final GCMParameterSpec spec = new GCMParameterSpec(128, ivBytes); cipher.init(Cipher.DECRYPT_MODE, secretKey, spec); decryptedBytes = cipher.doFinal(encryptedBytes); } catch (Throwable e) { e.printStackTrace(); } return decryptedBytes; }
Probando el Ejemplo
¡Ahora podemos probar nuestro ejemplo!
@TargetApi(Build.VERSION_CODES.M) private void testEncryption() { try { //Generate a key and store it in the KeyStore final KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder("MyKeyAlias", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) //.setUserAuthenticationRequired(true) //requires lock screen, invalidated if lock screen is disabled //.setUserAuthenticationValidityDurationSeconds(120) //only available x seconds from password authentication. -1 requires finger print - every time .setRandomizedEncryptionRequired(true) //different ciphertext for same plaintext on each call .build(); keyGenerator.init(keyGenParameterSpec); keyGenerator.generateKey(); //Test final HashMap<String, byte[]> map = encrypt("My very sensitive string!".getBytes("UTF-8")); final byte[] decryptedBytes = decrypt(map); final String decryptedString = new String(decryptedBytes, "UTF-8"); Log.e("MyApp", "The decrypted string is " + decryptedString); } catch (Throwable e) { e.printStackTrace(); } }
Uso de Claves Asimétricas RSA para Dispositivos Más Viejos
Esta es una buena solución para almacenar los datos para las versiones M y más, pero lo que si su aplicación es compatible con versiones anteriores? Claves simétricas de AES no son compatibles bajo M, son claves asimétricas RSA. Eso significa que podemos usar claves RSA y cifrado para lograr lo mismo.
La principal diferencia aquí es que un par de claves asimétrica contiene dos claves, una privada y una clave pública, donde la clave pública cifra los datos y descifra la clave privada. Una KeyPairGeneratorSpec
se pasa a la KeyPairGenerator
que se inicializa con KEY_ALGORITHM_RSA
y el proveedor de "AndroidKeyStore"
.
private void testPreMEncryption() { try { //Generate a keypair and store it in the KeyStore KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); Calendar start = Calendar.getInstance(); Calendar end = Calendar.getInstance(); end.add(Calendar.YEAR, 10); KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(this) .setAlias("MyKeyAlias") .setSubject(new X500Principal("CN=MyKeyName, O=Android Authority")) .setSerialNumber(new BigInteger(1024, new Random())) .setStartDate(start.getTime()) .setEndDate(end.getTime()) .setEncryptionRequired() //on API level 18, encrypted at rest, requires lock screen to be set up, changing lock screen removes key .build(); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); keyPairGenerator.initialize(spec); keyPairGenerator.generateKeyPair(); //Encryption test final byte[] encryptedBytes = rsaEncrypt("My secret string!".getBytes("UTF-8")); final byte[] decryptedBytes = rsaDecrypt(encryptedBytes); final String decryptedString = new String(decryptedBytes, "UTF-8"); Log.e("MyApp", "Decrypted string is " + decryptedString); } catch (Throwable e) { e.printStackTrace(); } }
Para cifrar, obtener la RSAPublicKey
del par de claves y utilizar con el objeto Cipher
.
public byte[] rsaEncrypt(final byte[] decryptedBytes) { byte[] encryptedBytes = null; try { final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry("MyKeyAlias", null); final RSAPublicKey publicKey = (RSAPublicKey)privateKeyEntry.getCertificate().getPublicKey(); final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); final CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher); cipherOutputStream.write(decryptedBytes); cipherOutputStream.close(); encryptedBytes = outputStream.toByteArray(); } catch (Throwable e) { e.printStackTrace(); } return encryptedBytes; }
Decodificación se realiza mediante el objeto RSAPrivateKey
.
public byte[] rsaDecrypt(final byte[] encryptedBytes) { byte[] decryptedBytes = null; try { final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry("MyKeyAlias", null); final RSAPrivateKey privateKey = (RSAPrivateKey)privateKeyEntry.getPrivateKey(); final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL"); cipher.init(Cipher.DECRYPT_MODE, privateKey); final CipherInputStream cipherInputStream = new CipherInputStream(new ByteArrayInputStream(encryptedBytes), cipher); final ArrayList<Byte> arrayList = new ArrayList<>(); int nextByte; while ( (nextByte = cipherInputStream.read()) != -1 ) { arrayList.add((byte)nextByte); } decryptedBytes = new byte[arrayList.size()]; for(int i = 0; i < decryptedBytes.length; i++) { decryptedBytes[i] = arrayList.get(i); } } catch (Throwable e) { e.printStackTrace(); } return decryptedBytes; }
Una cosa acerca de RSA es que la encriptación es más lenta de lo que es en AES. Esto es generalmente muy bien para pequeñas cantidades de información, como cuando usted está asegurando cadenas de preferencia compartidos. Si encuentras que hay un problema de rendimiento cifrar grandes cantidades de datos, sin embargo, puede en su lugar utilizar este ejemplo para cifrar y almacenar sólo una clave de AES. A continuación, utilice esa encriptación AES más rápida que se discutió en el tutorial anterior para el resto de sus datos. Puede generar una nueva clave AES y convertirla en una matriz de byte[]
que es compatible con este ejemplo.
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); keyGenerator.init(256); //AES-256 SecretKey secretKey = keyGenerator.generateKey(); byte[] keyBytes = secretKey.getEncoded();
Para recuperar la clave de los bytes, haga lo siguiente:
SecretKey key = new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
¡Era un montón de código! Para mantener todos los ejemplos simples, he omitido exhaustiva de excepciones. Pero recuerde que su código de producción, se recomienda no simplemente todos casos Throwable
en una captura declaración.
Conclusión
Esto completa el tutorial sobre las credenciales y claves. Gran parte de la confusión alrededor de las teclas y el almacenamiento tiene que ver con la evolución del sistema operativo Android, pero puede que la solución a utilizar dado el nivel de API su aplicación soporta.
Ahora que hemos cubierto las mejores prácticas para asegurar datos en reposo, el siguiente tutorial se centrará en la protección de datos en tránsito.