it-swarm.com.de

Verschlüsselungsfehler unter Android 4.2

Der folgende Code funktioniert mit allen Android-Versionen außer 4.2

import Java.security.InvalidKeyException;
import Java.security.NoSuchAlgorithmException;
import Java.security.SecureRandom;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

/**
 * Util class to perform encryption/decryption over strings. <br/>
 */
public final class UtilsEncryption
{
    /** The logging TAG */
    private static final String TAG = UtilsEncryption.class.getName();

    /** */
    private static final String KEY = "some_encryption_key";

    /**
     * Avoid instantiation. <br/>
     */
    private UtilsEncryption()
    {
    }

    /** The HEX characters */
    private final static String HEX = "0123456789ABCDEF";

    /**
     * Encrypt a given string. <br/>
     * 
     * @param the string to encrypt
     * @return the encrypted string in HEX
     */
    public static String encrypt( String cleartext )
    {
        try
        {
            byte[] result = process( Cipher.ENCRYPT_MODE, cleartext.getBytes() );
            return toHex( result );
        }
        catch ( Exception e )
        {
            System.out.println( TAG + ":encrypt:" + e.getMessage() );
        }
        return null;
    }

    /**
     * Decrypt a HEX encrypted string. <br/>
     * 
     * @param the HEX string to decrypt
     * @return the decrypted string
     */
    public static String decrypt( String encrypted )
    {
        try
        {
            byte[] enc = fromHex( encrypted );
            byte[] result = process( Cipher.DECRYPT_MODE, enc );
            return new String( result );
        }
        catch ( Exception e )
        {
            System.out.println( TAG + ":decrypt:" + e.getMessage() );
        }
        return null;
    }


    /**
     * Get the raw encryption key. <br/>
     * 
     * @param the seed key
     * @return the raw key
     * @throws NoSuchAlgorithmException
     */
    private static byte[] getRawKey()
        throws NoSuchAlgorithmException
    {
        KeyGenerator kgen = KeyGenerator.getInstance( "AES" );
        SecureRandom sr = SecureRandom.getInstance( "SHA1PRNG" );
        sr.setSeed( KEY.getBytes() );
        kgen.init( 128, sr );
        SecretKey skey = kgen.generateKey();
        return skey.getEncoded();
    }

    /**
     * Process the given input with the provided mode. <br/>
     * 
     * @param the cipher mode
     * @param the value to process
     * @return the processed value as byte[]
     * @throws InvalidKeyException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     * @throws NoSuchAlgorithmException
     * @throws NoSuchPaddingException
     */
    private static byte[] process( int mode, byte[] value )
        throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException,     NoSuchAlgorithmException,
        NoSuchPaddingException
    {
        SecretKeySpec skeySpec = new SecretKeySpec( getRawKey(), "AES" );
        Cipher cipher = Cipher.getInstance( "AES" );
        cipher.init( mode, skeySpec );
        byte[] encrypted = cipher.doFinal( value );
        return encrypted;
    }

    /**
     * Decode an HEX encoded string into a byte[]. <br/>
     * 
     * @param the HEX string value
     * @return the decoded byte[]
     */
    protected static byte[] fromHex( String value )
    {
        int len = value.length() / 2;
        byte[] result = new byte[len];
        for ( int i = 0; i < len; i++ )
        {
            result[i] = Integer.valueOf( value.substring( 2 * i, 2 * i + 2 ), 16     ).byteValue();
        }
        return result;
    }

    /**
     * Encode a byte[] into an HEX string. <br/>
     * 
     * @param the byte[] value
     * @return the HEX encoded string
     */
    protected static String toHex( byte[] value )
    {
        if ( value == null )
        {
            return "";
        }
        StringBuffer result = new StringBuffer( 2 * value.length );
        for ( int i = 0; i < value.length; i++ )
        {
            byte b = value[i];

            result.append( HEX.charAt( ( b >> 4 ) & 0x0f ) );
            result.append( HEX.charAt( b & 0x0f ) );
        }
        return result.toString();
    }
}

Hier ist ein kleiner Unit-Test, den ich erstellt habe, um den Fehler zu reproduzieren

import junit.framework.TestCase;

public class UtilsEncryptionTest
    extends TestCase
{
    /** A random string */
    private static String ORIGINAL = "some string to test";

    /**
     * The HEX value corresponds to ORIGINAL. <br/>
     * If you change ORIGINAL, calculate the new value on one of this sites:
     * <ul>
     * <li>http://www.string-functions.com/string-hex.aspx</li>
     * <li>http://www.yellowpipe.com/yis/tools/encrypter/index.php</li>
     * <li>http://www.convertstring.com/EncodeDecode/HexEncode</li>
     * </ul>
     */
    private static String HEX = "736F6D6520737472696E6720746F2074657374";

    public void testToHex()
    {
         String hexString = UtilsEncryption.toHex( ORIGINAL.getBytes() );

         assertNotNull( "The HEX string should not be null", hexString );
         assertTrue( "The HEX string should not be empty", hexString.length() > 0 );
         assertEquals( "The HEX string was not encoded correctly", HEX, hexString );
    }

    public void testFromHex()
    {
         byte[] stringBytes = UtilsEncryption.fromHex( HEX );

         assertNotNull( "The HEX string should not be null", stringBytes );
        assertTrue( "The HEX string should not be empty", stringBytes.length > 0 );
        assertEquals( "The HEX string was not encoded correctly", ORIGINAL, new String( stringBytes ) );
    }

    public void testWholeProcess()
    {
         String encrypted = UtilsEncryption.encrypt( ORIGINAL );
         assertNotNull( "The encrypted result should not be null", encrypted );
         assertTrue( "The encrypted result should not be empty", encrypted.length() > 0 );

         String decrypted = UtilsEncryption.decrypt( encrypted );
         assertNotNull( "The decrypted result should not be null", decrypted );
         assertTrue( "The decrypted result should not be empty", decrypted.length() > 0 );

         assertEquals( "Something went wrong", ORIGINAL, decrypted );
}

}

Die Zeile, die die Ausnahme auslöst, lautet:

byte[] encrypted = cipher.doFinal( value );

Die vollständige Stapelverfolgung ist:

    W/<package>.UtilsEncryption:decrypt(16414): pad block corrupted
    W/System.err(16414): javax.crypto.BadPaddingException: pad block corrupted
    W/System.err(16414):    at com.Android.org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher.engineDoFinal(BaseBlockCipher.Java:709)
    W/System.err(16414):    at javax.crypto.Cipher.doFinal(Cipher.Java:1111)
    W/System.err(16414):    at <package>.UtilsEncryption.process(UtilsEncryption.Java:117)
    W/System.err(16414):    at <package>.UtilsEncryption.decrypt(UtilsEncryption.Java:69)
    W/System.err(16414):    at <package>.UtilsEncryptionTest.testWholeProcess(UtilsEncryptionTest.Java:74)
    W/System.err(16414):    at Java.lang.reflect.Method.invokeNative(Native Method)
    W/System.err(16414):    at Java.lang.reflect.Method.invoke(Method.Java:511)
    W/System.err(16414):    at junit.framework.TestCase.runTest(TestCase.Java:168)
    W/System.err(16414):    at junit.framework.TestCase.runBare(TestCase.Java:134)
    W/System.err(16414):    at junit.framework.TestResult$1.protect(TestResult.Java:115)
    W/System.err(16414):    at junit.framework.TestResult.runProtected(TestResult.Java:133)
D/elapsed (  588): 14808
    W/System.err(16414):    at junit.framework.TestResult.run(TestResult.Java:118)
    W/System.err(16414):    at junit.framework.TestCase.run(TestCase.Java:124)
    W/System.err(16414):    at Android.test.AndroidTestRunner.runTest(AndroidTestRunner.Java:190)
    W/System.err(16414):    at Android.test.AndroidTestRunner.runTest(AndroidTestRunner.Java:175)
    W/System.err(16414):    at Android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.Java:555)
    W/System.err(16414):    at Android.app.Instrumentation$InstrumentationThread.run(Instrumentation.Java:1661)

Hat jemand eine Ahnung, was passieren könnte? Ist jemandem bekannt, dass sich in einer der referenzierten Klassen Android 4.2 grundlegend geändert hat? 

Danke vielmals

29
Robert Estivill

Von der Android Jellybean-Seite :

Die Standardimplementierungen von SecureRandom und Cipher.RSA wurden geändert, um OpenSSL zu verwenden

Sie änderten den Standardanbieter für SecureRandom, um OpenSSL anstelle des vorherigen Crypto-Providers zu verwenden.

Mit dem folgenden Code werden zwei verschiedene Ausgaben unter Android 4.2 und Android 4.2 erzeugt:

SecureRandom Rand = SecureRandom.getInstance("SHA1PRNG");
Log.i(TAG, "Rand.getProvider(): " + Rand.getProvider().getName());

Bei Geräten vor 4.2:

Rand.getProvider: Crypto

Bei 4.2 Geräten:

Rand.getProvider: AndroidOpenSSL

Zum Glück kann man leicht zum alten Verhalten zurückkehren:

SecureRandom sr = SecureRandom.getInstance( "SHA1PRNG", "Crypto" );

Um sicher zu sein, ist es gefährlich, SecureRandom.setSeed im Hinblick auf die Javadocs zu nennen, die Folgendes angeben: 

Das Seeding von SecureRandom ist möglicherweise unsicher

Ein Seed ist ein Array von Bytes, das zur Erzeugung von Zufallszahlen verwendet wird. Um kryptographisch sichere Zufallszahlen zu erzeugen, müssen sowohl der Startwert als auch der Algorithmus sicher sein.

Standardmäßig erzeugen Instanzen dieser Klasse einen ersten Startwert unter Verwendung einer internen Entropiequelle, z. B./dev/urandom. Dieses Saatgut ist unvorhersehbar und für eine sichere Verwendung geeignet.

Sie können den ursprünglichen Startwert auch explizit mit dem Seed-Konstruktor angeben oder indem Sie setSeed (Byte []) aufrufen, bevor Zufallszahlen generiert werden. Wenn Sie einen festen Startwert angeben, gibt die Instanz eine vorhersagbare Zahlenfolge zurück. Dies kann zum Testen nützlich sein, ist aber für eine sichere Verwendung nicht geeignet.

Für das Schreiben von Komponententests ist setSeed jedoch möglicherweise in Ordnung.

61
Brigham

Wie Brigham wies darauf hin, dass es in Android 4.2 eine Sicherheitsverbesserung gab, mit der die Standardimplementierung von SecureRandom von Crypto auf OpenSSL aktualisiert wurde

Cryptography - Die Standardimplementierungen von SecureRandom und Cipher.RSA wurden geändert, um OpenSSL zu verwenden. Unterstützung für SSL-Socket Für TLSv1.1 und TLSv1.2 mit OpenSSL 1.0.1 hinzugefügt

die Antwort von Bu Brigham ist eine vorübergehende Lösung und nicht empfehlenswert, da das Problem zwar gelöst wird, jedoch immer noch der falsche Weg ist. 

Die empfohlene Methode (check Nelenkovs Tutorial ) ist die Verwendung geeigneter Schlüsselableitungen PKCS (Public Key Cryptography Standard), die zwei Schlüsselableitungsfunktionen, PBKDF1 und PBKDF2, definiert, von denen PBKDF2 mehr empfohlen wird.

So sollten Sie den Schlüssel bekommen,

    int iterationCount = 1000;
    int saltLength = 8; // bytes; 64 bits
    int keyLength = 256;
    SecureRandom random = new SecureRandom();
    byte[] salt = new byte[saltLength];
    random.nextBytes(salt);
    KeySpec keySpec = new PBEKeySpec(seed.toCharArray(), salt,
            iterationCount, keyLength);
    SecretKeyFactory keyFactory = SecretKeyFactory
            .getInstance("PBKDF2WithHmacSHA1");
    byte[] raw = keyFactory.generateSecret(keySpec).getEncoded();
3
Sufiyan Ghori

Sie versuchen also, Pseudo-Zufallsgenerator als Schlüsselableitungsfunktion zu verwenden. Das ist aus folgenden Gründen schlecht:

  • PRNG sind von Natur aus nicht deterministisch, und Sie sind darauf angewiesen, deterministisch zu sein
  • Wenn Sie sich auf einen Fehler und veraltete Implementierungen verlassen, wird Ihre App eines Tages brechen
  • PRNG sind nicht als gute KDFs konzipiert

Genauer gesagt, Google die Verwendung des Crypto-Providers in Android N wurde nicht mehr unterstützt (SDK 24)

Hier sind einige bessere Methoden:

Hashed Message Authentication Code (HMAC) -basierte Schlüsselableitungsfunktion (HKDF)

Verwendung dieses Bibliothek :

String userInput = "this is a user input with bad entropy";

HKDF hkdf = HKDF.fromHmacSha256();

//extract the "raw" data to create output with concentrated entropy
byte[] pseudoRandomKey = hkdf.extract(staticSalt32Byte, userInput.getBytes(StandardCharsets.UTF_8));

//create expanded bytes for e.g. AES secret key and IV
byte[] expandedAesKey = hkdf.expand(pseudoRandomKey, "aes-key".getBytes(StandardCharsets.UTF_8), 16);

//Example boilerplate encrypting a simple string with created key/iv
SecretKey key = new SecretKeySpec(expandedAesKey, "AES"); //AES-128 key

PBKDF2 (Passwortbasierte Schlüsselableitungsfunktion 2)

hat key stretching , was es teurer macht, den Schlüssel brutal zu erzwingen. Verwenden Sie dies für schwache Tasteneingaben (z. B. Benutzerkennwort):

SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    KeySpec keySpec = new PBEKeySpec(passphraseOrPin, salt, iterations, outputKeyLength);
    SecretKey secretKey = secretKeyFactory.generateSecret(keySpec);
    return secretKey;

Es gibt mehrere KDFs wie BCrypt , scrypt und Argon2

0
patrickf