MASTG-KNOW-0012: Geração de Chaves

O Android SDK permite que você especifique como uma chave deve ser gerada e sob quais circunstâncias ela pode ser usada. O Android 6.0 (nível de API 23) introduziu a classe KeyGenParameterSpec, que pode ser usada para garantir o uso correto da chave no aplicativo. Por exemplo:

String keyAlias = "MySecretKey";

KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(keyAlias,
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
        .setRandomizedEncryptionRequired(true)
        .build();

KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,
        "AndroidKeyStore");
keyGenerator.init(keyGenParameterSpec);

SecretKey secretKey = keyGenerator.generateKey();

O KeyGenParameterSpec indica que a chave pode ser usada para criptografia e descriptografia, mas não para outros fins, como assinatura ou verificação. Ele especifica ainda o modo de bloco (CBC), o preenchimento (PKCS #7) e define explicitamente que a criptografia randomizada é necessária (este é o padrão). Em seguida, inserimos AndroidKeyStore como o nome do provedor na chamada KeyGenerator.getInstance para garantir que as chaves sejam armazenadas no Android KeyStore.

O GCM é um modo AES que fornece criptografia autenticada, aprimorando a segurança ao integrar criptografia e autenticação de dados em um único processo, diferentemente de modos mais antigos como o CBC, que exigem mecanismos separados como HMACs. Além disso, o GCM não requer preenchimento, o que simplifica a implementação e minimiza vulnerabilidades.

Tentar usar a chave gerada em violação à especificação acima resultaria em uma exceção de segurança.

Aqui está um exemplo de uso dessa chave para criptografar:

String AES_MODE = KeyProperties.KEY_ALGORITHM_AES
        + "/" + KeyProperties.BLOCK_MODE_CBC
        + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7;
KeyStore AndroidKeyStore = AndroidKeyStore.getInstance("AndroidKeyStore");

// byte[] input
Key key = AndroidKeyStore.getKey(keyAlias, null);

Cipher cipher = Cipher.getInstance(AES_MODE);
cipher.init(Cipher.ENCRYPT_MODE, key);

byte[] encryptedBytes = cipher.doFinal(input);
byte[] iv = cipher.getIV();
// salvar tanto o IV quanto os encryptedBytes

Tanto o IV (vetor de inicialização) quanto os bytes criptografados precisam ser armazenados; caso contrário, a descriptografia não será possível.

Aqui está como esse texto cifrado seria descriptografado. O input é o array de bytes criptografado e iv é o vetor de inicialização da etapa de criptografia:

// byte[] input
// byte[] iv
Key key = AndroidKeyStore.getKey(AES_KEY_ALIAS, null);

Cipher cipher = Cipher.getInstance(AES_MODE);
IvParameterSpec params = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, key, params);

byte[] result = cipher.doFinal(input);

Como o IV é gerado aleatoriamente a cada vez, ele deve ser salvo junto com o texto cifrado (encryptedBytes) para descriptografá-lo posteriormente.

Antes do Android 6.0 (nível de API 23), a geração de chaves AES não era suportada. Como resultado, muitas implementações optaram por usar RSA e gerar um par de chaves pública-privada para criptografia assimétrica usando KeyPairGeneratorSpec ou utilizar SecureRandom para gerar chaves AES.

Aqui está um exemplo de KeyPairGenerator e KeyPairGeneratorSpec usado para criar o par de chaves RSA:

Date startDate = Calendar.getInstance().getTime();
Calendar endCalendar = Calendar.getInstance();
endCalendar.add(Calendar.YEAR, 1);
Date endDate = endCalendar.getTime();
KeyPairGeneratorSpec keyPairGeneratorSpec = new KeyPairGeneratorSpec.Builder(context)
        .setAlias(RSA_KEY_ALIAS)
        .setKeySize(4096)
        .setSubject(new X500Principal("CN=" + RSA_KEY_ALIAS))
        .setSerialNumber(BigInteger.ONE)
        .setStartDate(startDate)
        .setEndDate(endDate)
        .build();

KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA",
        "AndroidKeyStore");
keyPairGenerator.initialize(keyPairGeneratorSpec);

KeyPair keyPair = keyPairGenerator.generateKeyPair();

Esta amostra cria o par de chaves RSA com um tamanho de chave de 4096 bits (tamanho do módulo). Chaves de Curva Elíptica (EC) também podem ser geradas de maneira similar. No entanto, a partir do Android 11 (nível de API 30), o AndroidKeyStore não suporta criptografia ou descriptografia com chaves EC. Elas só podem ser usadas para assinaturas.

Uma chave de criptografia simétrica pode ser gerada a partir de uma senha usando a Função de Derivação de Chave Baseada em Senha versão 2 (PBKDF2). Este protocolo criptográfico é projetado para gerar chaves criptográficas, que podem ser usadas para fins de criptografia. Os parâmetros de entrada para o algoritmo são ajustados de acordo com a seção função de derivação de chave inadequada. O código abaixo ilustra como gerar uma chave de criptografia forte baseada em uma senha.

public static SecretKey generateStrongAESKey(char[] password, int keyLength)
{
    //Inicializa objetos e variáveis para uso posterior
    int iterationCount = 10000;
    int saltLength     = keyLength / 8;
    SecureRandom random = new SecureRandom();
    //Gera o salt
    byte[] salt = new byte[saltLength];
    random.nextBytes(salt);
    KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iterationCount, keyLength);
    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
    return new SecretKeySpec(keyBytes, "AES");
}

O método acima requer um array de caracteres contendo a senha e o comprimento necessário da chave em bits, por exemplo, uma chave AES de 128 ou 256 bits. Definimos uma contagem de iterações de 10.000 rodades, que será usada pelo algoritmo PBKDF2. Aumentar o número de iterações aumenta significativamente o trabalho necessário para um ataque de força bruta na senha, no entanto, pode afetar o desempenho, pois mais poder computacional é necessário para a derivação da chave. Definimos o tamanho do salt igual ao comprimento da chave dividido por 8 para converter de bits para bytes e usamos a classe SecureRandom para gerar um salt aleatoriamente. O salt precisa ser mantido constante para garantir que a mesma chave de criptografia seja gerada repetidamente para a mesma senha fornecida. Observe que você pode armazenar o salt privadamente em SharedPreferences. É recomendado excluir o salt do mecanismo de backup do Android para evitar sincronização no caso de dados de maior risco.

Observe que, se você considerar um dispositivo root ou um aplicativo modificado (ex.: reempacotado) como uma ameaça aos dados, pode ser melhor criptografar o salt com uma chave que é colocada no Android KeyStore. A chave de criptografia baseada em senha (PBE) é gerada usando o algoritmo recomendado PBKDF2WithHmacSHA1 até o Android 8.0 (nível de API 26). Para níveis de API superiores, é melhor usar PBKDF2withHmacSHA256, que resultará em um valor de hash mais longo.

Nota: existe uma crença falsa generalizada de que o NDK deve ser usado para ocultar operações criptográficas e chaves embutidas no código. No entanto, usar esse mecanismo não é eficaz. Atacantes ainda podem usar ferramentas para encontrar o mecanismo usado e fazer despejos da chave na memória. Em seguida, o fluxo de controle pode ser analisado com, por exemplo, radare2, e as chaves extraídas com a ajuda do Frida ou a combinação de ambos: r2frida (consulte Desmontagem de Native Code e Exploração de Processos para mais detalhes). A partir do Android 7.0 (nível de API 24) em diante, não é permitido usar APIs privadas; em vez disso: APIs públicas precisam ser chamadas, o que impacta ainda mais a eficácia de ocultá-las, conforme descrito no Android Developers Blog.