descifrar - ¿Cuáles son las mejores prácticas para usar el cifrado AES en Android?
cifrar y descifrar en java (5)
Por qué hago esta pregunta:
Sé que ha habido muchas preguntas sobre el cifrado AES, incluso para Android. Y hay muchos fragmentos de código si busca en la Web. Pero en cada página, en cada pregunta sobre desbordamiento de pila, encuentro otra implementación con grandes diferencias.
Así que creé esta pregunta para encontrar una "mejor práctica". Espero que podamos recopilar una lista de los requisitos más importantes y establecer una implementación que sea realmente segura.
Leí sobre vectores de inicialización y sales. No todas las implementaciones que encontré tenían estas características. Entonces, ¿lo necesitas? ¿Aumenta mucho la seguridad? ¿Cómo lo implementas? ¿Debería el algoritmo generar excepciones si los datos cifrados no se pueden descifrar? ¿O es inseguro y debería devolver una cadena ilegible? ¿Puede el algoritmo usar Bcrypt en lugar de SHA?
¿Qué hay de estas dos implementaciones que encontré? ¿Están bien? Perfecto o algunas cosas importantes que faltan? ¿Qué de estos es seguro?
El algoritmo debe tomar una cadena y una "contraseña" para el cifrado y luego encriptar la cadena con esa contraseña. La salida debería ser una cadena (¿hex o base64?) De nuevo. El descifrado también debería ser posible, por supuesto.
¿Cuál es la implementación perfecta de AES para Android?
Implementación # 1:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class AdvancedCrypto implements ICrypto {
public static final String PROVIDER = "BC";
public static final int SALT_LENGTH = 20;
public static final int IV_LENGTH = 16;
public static final int PBE_ITERATION_COUNT = 100;
private static final String RANDOM_ALGORITHM = "SHA1PRNG";
private static final String HASH_ALGORITHM = "SHA-512";
private static final String PBE_ALGORITHM = "PBEWithSHA256And256BitAES-CBC-BC";
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final String SECRET_KEY_ALGORITHM = "AES";
public String encrypt(SecretKey secret, String cleartext) throws CryptoException {
try {
byte[] iv = generateIv();
String ivHex = HexEncoder.toHex(iv);
IvParameterSpec ivspec = new IvParameterSpec(iv);
Cipher encryptionCipher = Cipher.getInstance(CIPHER_ALGORITHM, PROVIDER);
encryptionCipher.init(Cipher.ENCRYPT_MODE, secret, ivspec);
byte[] encryptedText = encryptionCipher.doFinal(cleartext.getBytes("UTF-8"));
String encryptedHex = HexEncoder.toHex(encryptedText);
return ivHex + encryptedHex;
} catch (Exception e) {
throw new CryptoException("Unable to encrypt", e);
}
}
public String decrypt(SecretKey secret, String encrypted) throws CryptoException {
try {
Cipher decryptionCipher = Cipher.getInstance(CIPHER_ALGORITHM, PROVIDER);
String ivHex = encrypted.substring(0, IV_LENGTH * 2);
String encryptedHex = encrypted.substring(IV_LENGTH * 2);
IvParameterSpec ivspec = new IvParameterSpec(HexEncoder.toByte(ivHex));
decryptionCipher.init(Cipher.DECRYPT_MODE, secret, ivspec);
byte[] decryptedText = decryptionCipher.doFinal(HexEncoder.toByte(encryptedHex));
String decrypted = new String(decryptedText, "UTF-8");
return decrypted;
} catch (Exception e) {
throw new CryptoException("Unable to decrypt", e);
}
}
public SecretKey getSecretKey(String password, String salt) throws CryptoException {
try {
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), HexEncoder.toByte(salt), PBE_ITERATION_COUNT, 256);
SecretKeyFactory factory = SecretKeyFactory.getInstance(PBE_ALGORITHM, PROVIDER);
SecretKey tmp = factory.generateSecret(pbeKeySpec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), SECRET_KEY_ALGORITHM);
return secret;
} catch (Exception e) {
throw new CryptoException("Unable to get secret key", e);
}
}
public String getHash(String password, String salt) throws CryptoException {
try {
String input = password + salt;
MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM, PROVIDER);
byte[] out = md.digest(input.getBytes("UTF-8"));
return HexEncoder.toHex(out);
} catch (Exception e) {
throw new CryptoException("Unable to get hash", e);
}
}
public String generateSalt() throws CryptoException {
try {
SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
byte[] salt = new byte[SALT_LENGTH];
random.nextBytes(salt);
String saltHex = HexEncoder.toHex(salt);
return saltHex;
} catch (Exception e) {
throw new CryptoException("Unable to generate salt", e);
}
}
private byte[] generateIv() throws NoSuchAlgorithmException, NoSuchProviderException {
SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
byte[] iv = new byte[IV_LENGTH];
random.nextBytes(iv);
return iv;
}
}
Implementación # 2:
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* Usage:
* <pre>
* String crypto = SimpleCrypto.encrypt(masterpassword, cleartext)
* ...
* String cleartext = SimpleCrypto.decrypt(masterpassword, crypto)
* </pre>
* @author ferenc.hechler
*/
public class SimpleCrypto {
public static String encrypt(String seed, String cleartext) throws Exception {
byte[] rawKey = getRawKey(seed.getBytes());
byte[] result = encrypt(rawKey, cleartext.getBytes());
return toHex(result);
}
public static String decrypt(String seed, String encrypted) throws Exception {
byte[] rawKey = getRawKey(seed.getBytes());
byte[] enc = toByte(encrypted);
byte[] result = decrypt(rawKey, enc);
return new String(result);
}
private static byte[] getRawKey(byte[] seed) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
sr.setSeed(seed);
kgen.init(128, sr); // 192 and 256 bits may not be available
SecretKey skey = kgen.generateKey();
byte[] raw = skey.getEncoded();
return raw;
}
private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(clear);
return encrypted;
}
private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] decrypted = cipher.doFinal(encrypted);
return decrypted;
}
public static String toHex(String txt) {
return toHex(txt.getBytes());
}
public static String fromHex(String hex) {
return new String(toByte(hex));
}
public static byte[] toByte(String hexString) {
int len = hexString.length()/2;
byte[] result = new byte[len];
for (int i = 0; i < len; i++)
result[i] = Integer.valueOf(hexString.substring(2*i, 2*i+2), 16).byteValue();
return result;
}
public static String toHex(byte[] buf) {
if (buf == null)
return "";
StringBuffer result = new StringBuffer(2*buf.length);
for (int i = 0; i < buf.length; i++) {
appendHex(result, buf[i]);
}
return result.toString();
}
private final static String HEX = "0123456789ABCDEF";
private static void appendHex(StringBuffer sb, byte b) {
sb.append(HEX.charAt((b>>4)&0x0f)).append(HEX.charAt(b&0x0f));
}
}
Fuente: http://www.tutorials-android.com/learn/How_to_encrypt_and_decrypt_strings.rhtml
# 2 nunca debe usarse ya que solo usa "AES" (que significa encriptación del modo ECB en el texto, un gran no-no) para el cifrado. Voy a hablar sobre el # 1.
La primera implementación parece cumplir con las mejores prácticas para el cifrado. Las constantes generalmente son correctas, aunque tanto el tamaño de sal como el número de iteraciones para realizar PBE están en el lado corto. Además, parece ser para AES-256 ya que la generación de clave PBE usa 256 como un valor codificado (una pena después de todas esas constantes). Utiliza CBC y PKCS5Padding, que es al menos lo que cabría esperar.
Completamente falta una protección de autentificación / integridad, por lo que un atacante puede cambiar el texto de cifrado. Esto significa que los ataques de oráculo de relleno son posibles en un modelo cliente / servidor. También significa que un atacante puede intentar y cambiar los datos encriptados. Es probable que esto genere algún error en alguna parte debido a que la aplicación no acepta el relleno o el contenido, pero esa no es una situación en la que desea estar.
El manejo de excepciones y la validación de entradas se pueden mejorar, capturar Excepción siempre está mal en mi libro. Furhtermore, la clase implementa ICrypt, que no sé. Sé que tener solo métodos sin efectos secundarios en una clase es un poco extraño. Normalmente, harías esos estáticos. No hay buffering de instancias de Cipher, etc., por lo que cada objeto requerido se crea ad-nauseum. Sin embargo, puede eliminar ICrypto de forma segura de la definición que parece, en ese caso también podría refactorizar el código a métodos estáticos (o reescribirlo para que esté más orientado a objetos, a su elección).
El problema es que cualquier contenedor siempre hace suposiciones sobre el caso de uso. Decir que una envoltura es correcta o incorrecta es, por lo tanto, litera. Es por eso que siempre trato de evitar generar clases contenedoras. Pero al menos no parece explícitamente incorrecto.
Encontré una buena implementación aquí: http://nelenkov.blogspot.fr/2012/04/using-password-based-encryption-on.html y https://github.com/nelenkov/android-pbe Eso también fue útil en mi búsqueda de una implementación AES lo suficientemente buena para Android
Has hecho una pregunta bastante interesante. Como con todos los algoritmos, la clave de cifrado es la "salsa secreta", ya que una vez que es conocida por el público, todo lo demás también lo es. Entonces, busca formas de este documento por Google
Además, la facturación en la aplicación de Google también da ideas sobre la seguridad, que también es perspicaz
Use la API ligera de BouncyCastle. Proporciona 256 AES con PBE y sal.
Aquí muestra el código, que puede cifrar / descifrar archivos.
public void encrypt(InputStream fin, OutputStream fout, String password) {
try {
PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(new SHA256Digest());
char[] passwordChars = password.toCharArray();
final byte[] pkcs12PasswordBytes = PBEParametersGenerator.PKCS12PasswordToBytes(passwordChars);
pGen.init(pkcs12PasswordBytes, salt.getBytes(), iterationCount);
CBCBlockCipher aesCBC = new CBCBlockCipher(new AESEngine());
ParametersWithIV aesCBCParams = (ParametersWithIV) pGen.generateDerivedParameters(256, 128);
aesCBC.init(true, aesCBCParams);
PaddedBufferedBlockCipher aesCipher = new PaddedBufferedBlockCipher(aesCBC, new PKCS7Padding());
aesCipher.init(true, aesCBCParams);
// Read in the decrypted bytes and write the cleartext to out
int numRead = 0;
while ((numRead = fin.read(buf)) >= 0) {
if (numRead == 1024) {
byte[] plainTemp = new byte[aesCipher.getUpdateOutputSize(numRead)];
int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
final byte[] plain = new byte[offset];
System.arraycopy(plainTemp, 0, plain, 0, plain.length);
fout.write(plain, 0, plain.length);
} else {
byte[] plainTemp = new byte[aesCipher.getOutputSize(numRead)];
int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
int last = aesCipher.doFinal(plainTemp, offset);
final byte[] plain = new byte[offset + last];
System.arraycopy(plainTemp, 0, plain, 0, plain.length);
fout.write(plain, 0, plain.length);
}
}
fout.close();
fin.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void decrypt(InputStream fin, OutputStream fout, String password) {
try {
PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(new SHA256Digest());
char[] passwordChars = password.toCharArray();
final byte[] pkcs12PasswordBytes = PBEParametersGenerator.PKCS12PasswordToBytes(passwordChars);
pGen.init(pkcs12PasswordBytes, salt.getBytes(), iterationCount);
CBCBlockCipher aesCBC = new CBCBlockCipher(new AESEngine());
ParametersWithIV aesCBCParams = (ParametersWithIV) pGen.generateDerivedParameters(256, 128);
aesCBC.init(false, aesCBCParams);
PaddedBufferedBlockCipher aesCipher = new PaddedBufferedBlockCipher(aesCBC, new PKCS7Padding());
aesCipher.init(false, aesCBCParams);
// Read in the decrypted bytes and write the cleartext to out
int numRead = 0;
while ((numRead = fin.read(buf)) >= 0) {
if (numRead == 1024) {
byte[] plainTemp = new byte[aesCipher.getUpdateOutputSize(numRead)];
int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
// int last = aesCipher.doFinal(plainTemp, offset);
final byte[] plain = new byte[offset];
System.arraycopy(plainTemp, 0, plain, 0, plain.length);
fout.write(plain, 0, plain.length);
} else {
byte[] plainTemp = new byte[aesCipher.getOutputSize(numRead)];
int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
int last = aesCipher.doFinal(plainTemp, offset);
final byte[] plain = new byte[offset + last];
System.arraycopy(plainTemp, 0, plain, 0, plain.length);
fout.write(plain, 0, plain.length);
}
}
fout.close();
fin.close();
} catch (Exception e) {
e.printStackTrace();
}
}
Claves y Hashes
Comenzaré a discutir el sistema basado en contraseñas con sales. La sal es un número generado aleatoriamente. No es "deducido". La implementación 1 incluye un método generateSalt()
que genera un número aleatorio criptográficamente fuerte. Debido a que la sal es importante para la seguridad, debe mantenerse en secreto una vez que se genera, aunque solo necesita generarse una vez. Si se trata de un sitio web, es relativamente fácil mantener la sal en secreto, pero para las aplicaciones instaladas (para computadoras de escritorio y dispositivos móviles), esto será mucho más difícil, ya que se supone que dichas aplicaciones no guardan secretos.
El método getHash()
devuelve un hash de la contraseña y sal dados, concatenado en una sola cadena. El algoritmo utilizado es SHA-512, que devuelve un hash de 512 bits. Este método devuelve un hash que es útil para verificar la integridad de una cadena, por lo que también podría utilizarse llamando a getHash()
con solo una contraseña o simplemente un salt, ya que simplemente concatena ambos parámetros. Como este método no se usará en el sistema de encriptación basado en contraseñas, no lo discutiré más.
El método getSecretKey()
, deriva una clave de una matriz char
de la contraseña y una sal hexadecimal, como se devuelve desde generateSalt()
. El algoritmo utilizado es PBKDF1 (creo) de PKCS5 con SHA-256 como la función hash, y devuelve una clave de 256 bits. getSecretKey()
genera una clave generando hashes de la contraseña, salt y un contador (hasta el recuento de iteraciones dado en PBE_ITERATION_COUNT
, aquí 100) para aumentar el tiempo necesario para montar un ataque de fuerza bruta. La longitud de la sal debe ser al menos tan larga como la clave generada, en este caso, al menos 256 bits. El recuento de iteraciones se debe establecer el mayor tiempo posible sin causar demoras irrazonables. Para obtener más información sobre sales y recuentos de iteración en la derivación de clave, consulte la sección 4 en RFC2898 .
La implementación en el PBE de Java, sin embargo, es defectuosa si la contraseña contiene caracteres Unicode, es decir, aquellos que requieren más de 8 bits para ser representados. Como se indica en PBEKeySpec
, "el mecanismo de PBE definido en PKCS # 5 solo considera los 8 bits de orden inferior de cada carácter". Para evitar este problema, puede intentar generar una cadena hexadecimal (que contendrá solo caracteres de 8 bits) de todos los caracteres de 16 bits en la contraseña antes de pasarla a PBEKeySpec
. Por ejemplo, "ABC" se convierte en "004100420043". De hecho, también podría usar una matriz char como parámetro para la contraseña, ya que por razones de seguridad, PBEKeySpec "solicita la contraseña como una matriz char, por lo que puede sobrescribirse [con clearPassword()
] cuando clearPassword()
". No veo ningún problema, sin embargo, con la representación de una sal como una cadena codificada en hexadecimal.
Cifrado
Una vez que se genera una clave, podemos usarla para cifrar y descifrar texto. En la implementación 1, el algoritmo de cifrado utilizado es AES/CBC/PKCS5Padding
, es decir, AES en el modo de cifrado de Cipher Block Chaining (CBC), con relleno definido en PKCS # 5. (Otros modos de cifrado AES incluyen el modo contador (CTR), el modo libro electrónico electrónico (ECB) y el modo contador Galois (GCM). Otra pregunta sobre Desbordamiento de pila contiene respuestas que analizan en detalle los diversos modos de cifrado AES y los recomendados para usar. )
Si el texto encriptado estará disponible para personas externas, entonces se recomienda aplicar un código de autenticación de mensaje, o MAC, a los datos encriptados (y opcionalmente a parámetros adicionales) para proteger su integridad. Aquí son populares los MAC basados en hash, o HMAC, que se basan en SHA-1, SHA-256 u otras funciones hash seguras. Sin embargo, si se utiliza un MAC, se recomienda utilizar un secreto que sea al menos dos veces más largo que una clave de cifrado normal, para evitar los ataques clave relacionados: la primera mitad sirve como la clave de cifrado y la segunda mitad como la clave para el MAC. (Es decir, en este caso, genere un único secreto a partir de una contraseña y sal, y divida ese secreto en dos).
Implementación de Java
Las diversas funciones en la implementación 1 utilizan un proveedor específico, llamado "BC", para sus algoritmos. En general, sin embargo, no se recomienda solicitar proveedores específicos, ya que no todos los proveedores están disponibles en todas las implementaciones de Java, consulte Introducción a los proveedores de Oracle .
Por lo tanto, el PROVIDER
no debería existir y la cadena -BC
probablemente debería eliminarse de PBE_ALGORITHM
. La implementación 2 es correcta a este respecto.
No es apropiado que un método capture todas las excepciones, sino que solo maneje las excepciones que pueda. Las implementaciones dadas en su pregunta pueden arrojar una variedad de excepciones comprobadas. Un método puede elegir ajustar solo aquellas excepciones marcadas con CryptoException, o especificar aquellas excepciones marcadas en la cláusula throws
. Para mayor comodidad, puede ser apropiado ajustar la excepción original con CryptoException, ya que hay potencialmente muchas excepciones comprobadas que las clases pueden lanzar.