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 de Stack Overflow, 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 configurar 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 eso inseguro y debería devolver una cadena ilegible? ¿Puede el algoritmo usar Bcrypt en lugar de SHA?
¿Qué pasa con estas dos implementaciones que encontré? Estan bien ¿Perfecto o faltan algunas cosas importantes? ¿Qué de estos es seguro?
El algoritmo debe tomar una cadena y una "contraseña" para el cifrado y luego cifrar la cadena con esa contraseña. La salida debería ser una cadena (¿hexadecimal o base64?) Nuevamente. El descifrado también debería ser posible, por supuesto.
¿Cuál es la implementación AES perfecta 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
fuente
implements ICrypto
y cambiandothrows CryptoException
athrows Exception
y así sucesivamente. Entonces ya no necesitarás esas clases.getSecretKey
,getHash
,generateSalt
en la primera aplicación que no han sido utilizados. Tal vez esté equivocado, pero ¿cómo podría usarse esta clase para cifrar una cadena en la práctica?Respuestas:
Ninguna de las implementaciones que proporcione en su pregunta es del todo correcta, y ninguna de las implementaciones que proporcione debe usarse tal cual. A continuación, discutiré aspectos del cifrado basado en contraseña en Android.
Claves y hashes
Comenzaré a discutir el sistema basado en contraseñas con sales. La sal es un número generado aleatoriamente. No se "deduce". La implementación 1 incluye un
generateSalt()
método 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 debe generarse una vez. Si se trata de un sitio web, es relativamente fácil mantener el secreto de la sal, pero para las aplicaciones instaladas (para escritorio y dispositivos móviles), esto será mucho más difícil.El método
getHash()
devuelve un hash de la contraseña y la sal dadas, concatenados 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 usarse llamandogetHash()
con solo una contraseña o solo una sal, ya que simplemente concatena ambos parámetros. Dado que este método no se utilizará en el sistema de cifrado basado en contraseña, no lo discutiré más.El método
getSecretKey()
deriva una clave de unachar
matriz de la contraseña y un salt codificado en hexadecimal, como se devuelve degenerateSalt()
. El algoritmo utilizado es PBKDF1 (creo) de PKCS5 con SHA-256 como función hash, y devuelve una clave de 256 bits.getSecretKey()
genera una clave generando repetidamente hashes de la contraseña, sal y un contador (hasta el recuento de iteraciones indicadoPBE_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 que se genera, en este caso, al menos 256 bits. El recuento de iteraciones debe establecerse el mayor tiempo posible sin causar un retraso excesivo. Para obtener más información sobre las sales y los recuentos de iteraciones en la derivación de claves, 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 PBE definido en PKCS # 5 mira solo los 8 bits de orden inferior de cada carácter". Para solucionar 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 aPBEKeySpec
. Por ejemplo, "ABC" se convierte en "004100420043". Tenga en cuenta también que PBEKeySpec "solicita la contraseña como una matriz de caracteres, por lo que se puede sobrescribir [conclearPassword()
] cuando termine". (Con respecto a "proteger cadenas en la memoria", consulte esta pregunta ). Sin embargo, no veo ningún problema,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 Cipher Block Chaining (CBC), con relleno definido en PKCS # 5. (Otros modos de cifrado AES incluyen el modo de contador (CTR), el modo de libro de códigos electrónico (ECB) y el modo de contador de Galois (GCM). Otra pregunta sobre Stack Overflow contiene respuestas que discuten en detalle los diversos modos de cifrado AES y los recomendados para usar. Tenga en cuenta también que hay varios ataques al cifrado en modo CBC, algunos de los cuales se mencionan en RFC 7457.)Tenga en cuenta que debe utilizar un modo de cifrado que también verifique la integridad de los datos cifrados (por ejemplo, cifrado autenticado con datos asociados , AEAD, descrito en RFC 5116). Sin embargo,
AES/CBC/PKCS5Padding
no proporciona verificación de integridad, por lo que no se recomienda solo . Para propósitos de AEAD, se recomienda usar un secreto que sea al menos dos veces más largo que una clave de cifrado normal, para evitar ataques de clave relacionados: la primera mitad sirve como clave de cifrado y la segunda mitad sirve como clave para la verificación de integridad. (Es decir, en este caso, generar un único secreto a partir de una contraseña y sal, y dividir ese secreto en dos).Implementación de Java
Las diversas funciones de la implementación 1 utilizan un proveedor específico, a saber, "BC", para sus algoritmos. Sin embargo, en general, no se recomienda solicitar proveedores específicos, ya que no todos los proveedores están disponibles en todas las implementaciones de Java, ya sea por falta de soporte, para evitar la duplicación de código o por otras razones. Este consejo se ha vuelto especialmente importante desde el lanzamiento de la vista previa de Android P a principios de 2018, porque algunas funciones del proveedor "BC" han quedado obsoletas allí; consulte el artículo "Cambios en criptografía en Android P" en el Blog de desarrolladores de Android. Consulte también la Introducción a los proveedores de Oracle .
Por lo tanto,
PROVIDER
no debería existir y la cadena-BC
debería eliminarsePBE_ALGORITHM
. La implementación 2 es correcta a este respecto.No es apropiado que un método capture todas las excepciones, sino que maneje solo las excepciones que pueda. Las implementaciones dadas en su pregunta pueden generar una variedad de excepciones comprobadas. Un método puede optar por envolver solo las excepciones marcadas con CryptoException, o especificar esas excepciones marcadas en la
throws
cláusula. Por conveniencia, encapsular la excepción original con CryptoException puede ser apropiado aquí, ya que hay potencialmente muchas excepciones marcadas que las clases pueden lanzar.SecureRandom
en AndroidComo se detalla en el artículo "Some SecureRandom Thoughts", en el Blog de desarrolladores de Android, la implementación de
java.security.SecureRandom
versiones de Android antes de 2013 tiene una falla que reduce la fuerza de los números aleatorios que ofrece. Esta falla se puede mitigar como se describe en ese artículo.fuente
getInstance
tiene una sobrecarga que toma solo el nombre del algoritmo. Ejemplo: Cipher.getInstance () Varios proveedores, incluido Bouncy Castle, pueden estar registrados en la implementación de Java y este tipo de sobrecarga busca en la lista de proveedores uno de ellos que implemente el algoritmo dado. Deberías probarlo y verás.# 2 nunca debe usarse ya que solo usa "AES" (lo que significa cifrado en modo ECB en texto, un gran no-no) para el cifrado. Solo hablaré del # 1.
La primera implementación parece adherirse a las mejores prácticas para el cifrado. Las constantes son generalmente correctas, aunque tanto el tamaño de la 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 claves 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.
Falta por completo cualquier protección de autenticación / integridad, por lo que un atacante puede cambiar el texto cifrado. Esto significa que los ataques de relleno de Oracle son posibles en un modelo cliente / servidor. También significa que un atacante puede intentar cambiar los datos cifrados. Esto probablemente dará como resultado 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 entrada podrían mejorarse, la captura de excepciones siempre es incorrecta en mi libro. Además, 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, los haría estáticos. No hay almacenamiento en búfer de instancias de Cipher, etc., por lo que cada objeto requerido se crea ad-nauseum. Sin embargo, puede eliminar de forma segura ICrypto 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, su elección).
El problema es que cualquier contenedor siempre hace suposiciones sobre el caso de uso. Decir que un envoltorio está bien o mal es, por tanto, una tontería. Es por eso que siempre trato de evitar generar clases contenedoras. Pero al menos no parece explícitamente incorrecto.
fuente
Has hecho una pregunta bastante interesante. Al igual que con todos los algoritmos, la clave de cifrado es la "salsa secreta", ya que una vez que el público lo conoce, todo lo demás también lo es. Así que busca formas de acceder a este documento de Google
seguridad
Además de la facturación integrada en la aplicación de Google, también ofrece ideas sobre seguridad, lo que también es revelador
billing_best_practices
fuente
Utilice la API ligera de BouncyCastle. Proporciona 256 AES con PBE y Salt.
Aquí se muestra un 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(); } }
fuente
buf
( realmente espero que no sea unstatic
campo). También se parece a ambosencrypt()
ydecrypt()
no procesará correctamente el bloque final si la entrada es un múltiplo de 1024 bytes.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 de AES suficientemente buena para Android
fuente