代码之家  ›  专栏  ›  技术社区  ›  Rafael T

尝试在GCM模式下解密邮件时收到AEADBadTagException

  •  0
  • Rafael T  · 技术社区  · 6 年前

    我正在写一个有很多安全限制的应用程序:

    为了存档,我生成了 KeyPair 在我的电脑上,将公共部分放入我的应用程序,生成 AES SecretKey 在应用程序中设置密钥,加密并用我的公钥保存(用于操作员目的),然后将未加密的密钥放入 AndroidKeyStore .

    为了加密消息,我接收 KeyStore ,加密我的消息,获取 IV 我使用了encryptedSecretKey,并按定义的顺序将它们写入字节数组(iv->encryptedSecretKey->encryptedMessage)。

    若要解密,请反向尝试相同的操作:获取字节数组,读取iv和encryptedSecretKey,然后将其余的(encryptedMessage)传递给我的cypher进行解密。 问题是 cipher.doFinal(encryptedMessage) 正在抛出一个 javax.crypto.AEADBadTagException 是由 android.security.KeyStoreException: Signature/MAC verification failed .

    我已经检查了加密的消息和要解密的消息是否完全相同。我不知道我做错了什么。

    我使用的类别如下:

    package my.company.domain;
    
    import android.content.Context;
    import android.content.SharedPreferences;
    import android.security.keystore.KeyProperties;
    import android.security.keystore.KeyProtection;
    import android.support.annotation.NonNull;
    import android.util.Base64;
    import android.util.Log;
    
    import java.io.DataInputStream;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.nio.charset.StandardCharsets;
    import java.security.GeneralSecurityException;
    import java.security.InvalidKeyException;
    import java.security.KeyFactory;
    import java.security.KeyStore;
    import java.security.KeyStoreException;
    import java.security.NoSuchAlgorithmException;
    import java.security.NoSuchProviderException;
    import java.security.PublicKey;
    import java.security.SecureRandom;
    import java.security.spec.X509EncodedKeySpec;
    
    import javax.crypto.BadPaddingException;
    import javax.crypto.Cipher;
    import javax.crypto.IllegalBlockSizeException;
    import javax.crypto.NoSuchPaddingException;
    import javax.crypto.SecretKey;
    import javax.crypto.spec.GCMParameterSpec;
    import javax.crypto.spec.SecretKeySpec;
    
    public class CryptoHelper {
    
        public static final String TAG = CryptoHelper.class.getSimpleName();
    
        private static final String KEY_ALIAS = "OI1lTI1lLI1l0";
        private static final char[] KEY_PASSWORD = "Il0VELI1lO".toCharArray();
    
        private static final String PREF_NAME = "CryptoPrefs";
        private static final String KEY_ENCRYPTED_SECRET = "encryptedSecret";
    
        private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
    
        private static final int    IV_SIZE = 12;
        private static final int    IV_BIT_LEN = IV_SIZE * 8;
    
    
        //generate 128 bit key (16), other possible values 192(24), 256(32)
        private static final int    AES_KEY_SIZE = 16;
        private static final String AES = KeyProperties.KEY_ALGORITHM_AES;
        private static final String AES_MODE = AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/" + KeyProperties.ENCRYPTION_PADDING_NONE;
    
        private static final String RSA = KeyProperties.KEY_ALGORITHM_RSA;
        private static final String RSA_MODE = KeyProperties.KEY_ALGORITHM_RSA + "/" + KeyProperties.BLOCK_MODE_ECB + "/" + KeyProperties.ENCRYPTION_PADDING_NONE;
        private static final String RSA_PROVIDER = "AndroidOpenSSL";
    
        private final Context mContext;
        private final SharedPreferences mPrefs;
    
        private SecureRandom mSecureRandom;
        private KeyStore mAndroidKeyStore;
        private PublicKey mPublicKey;
        private byte[] mEncryptedSecretKey;
    
        public CryptoHelper(Context context) {
            mContext = context;
            mSecureRandom = new SecureRandom();
            mPrefs = mContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
            try {
                mAndroidKeyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
                mAndroidKeyStore.load(null);
    
            } catch (KeyStoreException e) {
                Log.wtf(TAG, "Could not get AndroidKeyStore!", e);
            } catch (Exception e) {
                Log.wtf(TAG, "Could not load AndroidKeyStore!", e);
            }
        }
    
        public void reset() throws KeyStoreException {
            mAndroidKeyStore.deleteEntry(KEY_ALIAS);
        }
    
        public byte[] encrypt(byte[] message){
            SecretKey secretKey = getSecretKey();
            try {
                Cipher cipher = Cipher.getInstance(AES_MODE);
                cipher.init(Cipher.ENCRYPT_MODE, getSecretKey());
    
                byte[] cryptedBytes = cipher.doFinal(message);
                byte[] iv = cipher.getIV();
                byte[] encryptedSecretKey = getEncryptedSecretKey();
                ByteBuffer buffer = ByteBuffer.allocate(IV_BIT_LEN + encryptedSecretKey.length + cryptedBytes.length);
                buffer
                    .put(iv)
                    .put(encryptedSecretKey)
                    .put(cryptedBytes);
                return buffer.array();
            } catch (GeneralSecurityException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public byte[] encrypt(String message){
            return encrypt(message.getBytes(StandardCharsets.UTF_8));
        }
    
        public byte[] decrypt(byte[] bytes){
            ByteBuffer buffer = ByteBuffer.wrap(bytes);
            byte[] iv = new byte[IV_SIZE];
            buffer.get(iv);
            byte[] unused = getEncryptedSecretKey();
            buffer.get(unused);
            byte[] encryptedMessage = new byte[bytes.length - IV_SIZE - unused.length];
            buffer.get(encryptedMessage);
            try {
                Cipher cipher = Cipher.getInstance(AES_MODE);
                GCMParameterSpec parameterSpec = new GCMParameterSpec(IV_BIT_LEN, iv);
                cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), parameterSpec);
                byte[] decryptedMessage = cipher.doFinal(encryptedMessage);
                return decryptedMessage;
            } catch (GeneralSecurityException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public String decryptToString(byte[] bytes){
            return new String(decrypt(bytes), StandardCharsets.UTF_8);
        }
    
        public byte[] decrypt(FileInputStream fileToDecrypt){
            byte[] buffer = null;
            try {
                buffer = new byte[fileToDecrypt.available()];
                fileToDecrypt.read(buffer);
                buffer = decrypt(buffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return buffer;
        }
    
    
        public PublicKey getPublicKey() {
            if (null == mPublicKey) {
                mPublicKey = readPublicKey();
            }
            return mPublicKey;
        }
    
        public byte[] getEncryptedSecretKey() {
            if (null == mEncryptedSecretKey){
                mEncryptedSecretKey = Base64.decode(mPrefs.getString(KEY_ENCRYPTED_SECRET, null), Base64.NO_WRAP);
            }
            return mEncryptedSecretKey;
        }
    
        private void saveEncryptedSecretKey(byte[] encryptedSecretKey){
            String base64EncryptedKey = Base64.encodeToString(encryptedSecretKey, Base64.NO_WRAP);
            mPrefs.edit().putString(KEY_ENCRYPTED_SECRET, base64EncryptedKey).apply();
        }
    
        protected SecretKey getSecretKey(){
            SecretKey secretKey = null;
            try {
                if (!mAndroidKeyStore.containsAlias(KEY_ALIAS)){
                   generateAndStoreSecureKey();
                }
                secretKey = (SecretKey) mAndroidKeyStore.getKey(KEY_ALIAS, KEY_PASSWORD);
            } catch (KeyStoreException e) {
                Log.wtf(TAG, "Could not check AndroidKeyStore alias!", e);
                secretKey = null;
            } catch (GeneralSecurityException e) {
                e.printStackTrace();
                secretKey = null;
            }
            return secretKey;
        }
    
        private void generateAndStoreSecureKey()
                throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, KeyStoreException, BadPaddingException, IllegalBlockSizeException {
            SecretKey secretKey = generateSecureRandomKey();
            PublicKey publicKey = getPublicKey();
            Cipher keyCipher = Cipher.getInstance(RSA_MODE, RSA_PROVIDER);
            keyCipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] encryptedSecretKeyBytes = keyCipher.doFinal(secretKey.getEncoded());
    
            saveEncryptedSecretKey(encryptedSecretKeyBytes);
    
            KeyProtection keyProtection = new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_VERIFY)
                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                    .build();
            mAndroidKeyStore.setEntry(KEY_ALIAS, new KeyStore.SecretKeyEntry(secretKey), keyProtection);
        }
    
    
        protected PublicKey readPublicKey() {
            DataInputStream dis = null;
            PublicKey key = null;
            try {
                dis = new DataInputStream(mContext.getResources().getAssets().open("public_key.der"));
                byte[] keyBytes = new byte[dis.available()];
                dis.readFully(keyBytes);
    
                X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
                KeyFactory facotory = KeyFactory.getInstance(RSA);
                key = facotory.generatePublic(spec);
            } catch (Exception e) {
                key = null;
            } finally {
                if (null != dis) {
                    try {
                        dis.close();
                    } catch (IOException e) {
                        Log.wtf(TAG, "Cannot Close Stream!", e);
                    }
                }
            }
            return key;
        }
    
        @NonNull
        protected SecretKey generateSecureRandomKey() {
            return new SecretKeySpec(generateSecureRandomBytes(AES_KEY_SIZE), AES);
        }
    
        @NonNull
        protected byte[] generateSecureRandomBytes(int byteCount) {
            byte[] keyBytes = new byte[byteCount];
            mSecureRandom.nextBytes(keyBytes);
            return keyBytes;
        }
    }
    

    我这样测试:

    @Test
    public void testCrypto() throws Exception {
        CryptoHelper crypto = new CryptoHelper(InstrumentationRegistry.getTargetContext());
        crypto.reset();
        String verySecretOpinion = "we're all doomed";
        byte[] encrypt = crypto.encrypt(verySecretOpinion);
        Assert.assertNotNull("Encrypted secret is Null!", encrypt);
        Assert.assertFalse("encrypted Bytes are the same as Input!", new String(encrypt, StandardCharsets.UTF_8).equals(verySecretOpinion));
        String decryptedString = crypto.decryptToString(encrypt);
        Assert.assertNotNull("Decrypted String must be Non-Null!", decryptedString);
        Assert.assertEquals("Decrypted String doesn't equal encryption input!", verySecretOpinion, decryptedString);
    }
    

    顺便说一下,minSdkVersion是25,比棉花糖还高

    更新

    1. 固定的 Cipher.DECRYPT_MODE ENCRYPT_MODE 关于将密钥thx保存到James K Polk的评论中
    2. BlockMode GCM 到块模式 CBC (并更改 GCMParameterSpec IvParamterSpec
    1 回复  |  直到 6 年前
        1
  •  2
  •   President James K. Polk    6 年前

    操作员界面至少有两个问题。首先,RSA使用错误的密码模式对密钥进行加密:在应该使用encrypt的情况下使用了DECRYPT模式。其次,您使用的RSA没有任何填充。你需要使用一个真正的填充模式,其中一个OEAP填充模式是推荐的。

    当调整用于保存结果的缓冲区大小时,加密端出错:

    ByteBuffer buffer = ByteBuffer.allocate(IV_BIT_LEN + encryptedSecretKey.length + cryptedBytes.length);
    

    分配太多空间。 IV_BIT_LEN 应该改成 IV_SIZE ByteBuffer .

    最后一个错误是在设置 GCMParameterSpec 在解密方面。您初始化了此行中的标记长度

    GCMParameterSpec parameterSpec = new GCMParameterSpec(IV_BIT_LEN, iv);
    

    您可以通过调用 cipher.getParameters().getParameterSpec(GCMParameterSpec.class) 要获得参数规范,您可以从中检索标记长度和iv。您可能应该将标记长度(16字节=128位)视为硬编码常量,而不是传输它。接收器应类似地假设标签长度为128位。