【问题标题】:Encrypted save and decrypted load of an ArrayList of serializable objects可序列化对象的 ArrayList 的加密保存和解密加载
【发布时间】:2017-11-21 17:42:19
【问题描述】:

我使用这两种方法在 sd 卡中保存并加载一个包含可序列化对象ArrayList 的文件

保存方法

public static void saveUserList(ArrayList<User> userList) {
        if (storageAvailable()) {
            try {
                createFolder();

                FileOutputStream userList = new FileOutputStream(
                        baseDir + File.separator + baseAppDir + File.separator
                                + fileName);

                ObjectOutputStream oos = new ObjectOutputStream(
                        userList);
                oos.writeObject(userList);

                oos.close();
            } catch (Exception exc) {
                exc.printStackTrace();
            }
        }

    }

加载方法

 public static ArrayList<User> loadUserList() {
        if (storageAvailable()) {
            ArrayList<User> userList = new ArrayList<User>();
            try {
                FileInputStream userList = new FileInputStream(baseDir
                        + File.separator + baseAppDir + File.separator
                        + fileName);

                ObjectInputStream oos = new ObjectInputStream(
                        userList);

                userList = (ArrayList<User>) oos.readObject();
                oos.close();

            } catch (Exception exc) {
                exc.printStackTrace();
            }

            return userList;
        } else {
            return null;
        }

    }

现在我希望saveUserList 方法在保存期间根据特定的String keyword 加密文件的内容,而loadUserList 方法使用相同的关键字解密文件以返回arrayList。

我怎么能这样做? 我已经看过CipherOutputStream,但我不明白我应该如何使用它。

建议使用Conceal库的方法

public static void saveUserListCrypted(ArrayList<User> userList) {
    if (storageAvailable()) {
        try {
            createFolder();
            Crypto crypto = new Crypto(
                new SharedPrefsBackedKeyChain(context),
                new SystemNativeCryptoLibrary());

            FileOutputStream userList = new FileOutputStream(
                    baseDir + File.separator + baseAppDir + File.separator
                            + fileName);

            OutputStream cryptedStream = crypto.getCipherOutputStream(
                userList, new Entity("UserList");


            ObjectOutputStream oos = new ObjectOutputStream(
                    cryptedStream);
            oos.writeObject(userList);

            oos.close();
        } catch (Exception exc) {
            exc.printStackTrace();
        }
    }

}

导致这个错误

 this error java.lang.UnsupportedOperationException 02-12 21:29:05.026 2051-2051/com.myapp W/System.err﹕ at com.facebook.crypto.streams.NativeGCMCipherOutputStream.write

【问题讨论】:

    标签: java android encryption file-io


    【解决方案1】:

    尝试(添加适当的检查并尝试我省略的块以使代码更具可读性)保存类似这样的内容

    public static void AESObjectEncoder(Serializable object, String password, String path) {
            try {
                Cipher cipher = null;
                cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
                cipher.init(Cipher.ENCRYPT_MODE, fromStringToAESkey(password));
                SealedObject sealedObject = null;
                sealedObject = new SealedObject(object, cipher);
                CipherOutputStream cipherOutputStream = null;
                cipherOutputStream = new CipherOutputStream(new BufferedOutputStream(new FileOutputStream(path)), cipher);
                ObjectOutputStream outputStream = null;
                outputStream = new ObjectOutputStream(cipherOutputStream);
                outputStream.writeObject(sealedObject);
                outputStream.close();    
        }
    

    这个要加载

     public static Serializable AESObjectDedcoder(String password, String path) {
            Cipher cipher = null;
            Serializable userList = null;
            cipher = Cipher.getInstance("AES/CBC/PKCS7Pdding");
    
            //Code to write your object to file
            cipher.init(Cipher.DECRYPT_MODE, fromStringToAESkey(password));         
            CipherInputStream cipherInputStream = null;
            cipherInputStream = new CipherInputStream(new BufferedInputStream(new FileInputStream(path)), cipher);
    
            ObjectInputStream inputStream = null;
            inputStream = new ObjectInputStream(cipherInputStream);
            SealedObject sealedObject = null;
            sealedObject = (SealedObject) inputStream.readObject();
            userList = (Serializable) sealedObject.getObject(ciper);  
            return userList;
        }
    

    要从字符串创建SecretKey,您可以使用它

    public static SecretKey fromStringToAESkey(String s) {
            //256bit key need 32 byte
            byte[] rawKey = new byte[32];
            // if you don't specify the encoding you might get weird results
            byte[] keyBytes = new byte[0];
            try {
                keyBytes = s.getBytes("ASCII");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            System.arraycopy(keyBytes, 0, rawKey, 0, keyBytes.length);
            SecretKey key = new SecretKeySpec(rawKey, "AES");
            return key;
        }
    

    注意:

    这段代码加解密两次,展示了密封对象和密码流的使用方式

    【讨论】:

    • 谢谢,但不幸的是我得到了java.lang.IllegalStateException: Cipher not initialized
    • mmm...创建一个新问题,提供有关错误的更多详细信息
    • 我会用像 PBKDF 这样的标准密钥派生函数替换 fromStringToAESkey。这通常会提供更好的分布式密钥。
    • 为什么要加解密两次?这真的有效吗?首先,Cipher 对象的状态不会正确结合。
    • 加密和解密两次以显示密封对象和密码流的使用方式,但是它的工作原理相同。您可以在投反对票之前尝试看看自己。编译此代码只需要基本调整(如适当的 try-catch 块)。
    【解决方案2】:

    我建议看看 Facebook 最近发布的 Conceal:http://facebook.github.io/conceal/

    这应该是用当前代码中使用的 ObjectOutputStream 包装 Conceal 输出流的简单修改:

    public static void saveUserList(ArrayList<User> userList) {
        if (storageAvailable()) {
            try {
                createFolder();
                Crypto crypto = new Crypto(
                    new SharedPrefsBackedKeyChain(context),
                    new SystemNativeCryptoLibrary());
    
                FileOutputStream userList = new FileOutputStream(
                        baseDir + File.separator + baseAppDir + File.separator
                                + fileName);
    
                OutputStream cryptedStream = crypto.getCipherOutputStream(
                    userList, new Entity("UserList");
    
    
                ObjectOutputStream oos = new ObjectOutputStream(
                        cryptedStream);
                oos.writeObject(userList);
    
                oos.close();
            } catch (Exception exc) {
                exc.printStackTrace();
            }
        }
    
    }
    

    我将把还原作为练习留给读者。 ;)

    【讨论】:

    • 谢谢你的回答,你能给我更多关于这个实现的细节吗?关键是上下文?如果是,如果我尝试从另一个 Activity 调用类似的恢复方法无法解密?
    • 没有。密钥存储在 SharedPreference 后端,可从所有活动访问。阅读图书馆上的文档以获取更多信息。
    • 我试过了,但得到这个错误java.lang.UnsupportedOperationException 02-12 21:29:05.026 2051-2051/com.myapp W/System.err﹕ at com.facebook.crypto.streams.NativeGCMCipherOutputStream.write
    • 你有链接原生库吗?无论如何,我建议打开一个新问题,提供有关此特定错误的更多信息。
    • 我已在 libs 文件夹中添加了本机库,它们各自的 'armeabi, armeabi=v7` 和 x86 子文件夹。我有什么遗漏的吗?
    【解决方案3】:

    您可以简单地使用 AES 编码:

    private static byte[] getEncrypt(final String key, final String message) throws GeneralSecurityException {
      final byte[] rawData = key.getBytes(Charset.forName("US-ASCII"));
      if (rawData.length != 16) {
        // If this is not 16 in length, there's a problem with the key size, nothing to do here
        throw new IllegalArgumentException("You've provided an invalid key size");
      }
    
      final SecretKeySpec seckeySpec = new SecretKeySpec(rawData, "AES");
      final Cipher ciph = Cipher.getInstance("AES/CBC/PKCS5Padding");
    
      ciph.init(Cipher.ENCRYPT_MODE, seckeySpec, new IvParameterSpec(new byte[16]));
      return ciph.doFinal(message.getBytes(Charset.forName("US-ASCII")));
    }
    
    private static String getDecrypt(String key, byte[] encrypted) throws GeneralSecurityException {
      final byte[] rawData = key.getBytes(Charset.forName("US-ASCII"));
      if (rawData.length != 16) {
        // If this is not 16 in length, there's a problem with the key size, nothing to do here
        throw new IllegalArgumentException("Invalid key size.");
      }
    
      final SecretKeySpec seckeySpec = new SecretKeySpec(rawData, "AES");
    
      final Cipher ciph = Cipher.getInstance("AES/CBC/PKCS5Padding");
      ciph.init(Cipher.DECRYPT_MODE, seckeySpec, new IvParameterSpec(new byte[16]));
      final byte[] decryptedmess = ciph.doFinal(encrypted);
    
      return new String(decryptedmess, Charset.forName("US-ASCII"));
    }
    

    那么,试一试,这个例子可能对你有效:

    final String encrypt = "My16inLengthKey5";
    final byte[] docrypt = getEncrypt(encrypt, "This is a phrase to be encrypted!");
    final String douncrypt = getDecrypt(encrypt.toString(), docrypt);
    Log.d("Decryption", "Decrypted phrase: " + douncrypt);
    

    当然,douncrypt 必须匹配 This is a phrase to be encrypted!

    【讨论】:

    • 我收到 getBytes 的“调用需要 API 级别 9”。我可以为 Android KitKat API 19 做什么?而且对于这一行,我也得到了同样的结果“ new String(decryptedmess, Charset.forName("US-ASCII"));”
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-14
    • 1970-01-01
    相关资源
    最近更新 更多