自动加密可序列化的类

在Coursera安全性最高项目的验尸讨论中提出了一个疯狂的想法。 类可以在序列化期间对其自身进行加密吗?

这主要是一项学术性的“假设”练习。 很难想到这样一种情况,我们希望在持久性期间依靠对象自加密而不是使用显式加密机制。 我只能确定一种情况,我们不能简单地使类无法序列化:

HTTPSession钝化

Appserver可以钝化不活动的HTTPSession,以节省空间或将会话从一台服务器迁移到另一台服务器。 这就是会话应该只包含可序列化对象的原因。 (在可以安装在单个服务器上的小型应用程序中,通常会忽略此限制,但是如果需要扩展或扩展实现,则会导致问题。)

一种方法(也是首选方法?)是使会话在钝化过程中将其自身写入数据库,并在激活过程中将其自身重新加载。 实际保留的唯一信息是重新加载数据所需的内容,通常只是用户ID。 这给HTTPSession实现增加了一些复杂性,但是有很多好处。 一个主要好处是确保敏感信息被加密很简单。

这不是唯一的方法,某些站点可能更喜欢使用标准序列化。 一些应用服务器可能会将“实时”会话的序列化副本的副本保留在H2等嵌入式数据库中。 谨慎的开发人员可能希望确保敏感信息在序列化期间进行加密,即使它永远不会发生。

注意:可以提出一个强烈的论点,即敏感信息不应该首先出现在会话中–仅在必要时检索它,并在不再需要时安全地丢弃它。

该方法

我采用的方法基于有效Java中的序列化一章。 广义上讲,我们希望使用序列化代理来处理实际的加密。 该行为是:

行动 方法 受保护的序列化类 序列化代理
序列化 writeReplace() 创建代理 不适用
writeObject() 抛出异常 将加密的内容写入ObjectOutputStream
反序列化 readObject() 从ObjectInputStream读取加密的内容
readResolve() 构造受保护的类对象


调用反序列化方法时,受保护的类引发异常的原因是,它防止了攻击者生成的序列化对象的攻击。 请参阅上述书籍中有关虚假字节流攻击和内部字段盗窃攻击的讨论。

这种方法有很大的局限性-如果没有子类重新实现代理,则无法扩展该类。 我认为这不是实际问题,因为该技术仅用于保护包含敏感信息的类,并且很少希望添加超出设计人员期望的方法的方法。

代理类处理加密。 下面的实现显示了使用随机盐(IV)和加密强消息摘要(HMAC)来检测篡改。

编码

public class ProtectedSecret implements Serializable {
    private static final long serialVersionUID = 1L;

    private final String secret;

    /**
     * Constructor.
     * 
     * @param secret
     */
    public ProtectedSecret(final String secret) {
        this.secret = secret;
    }

    /**
     * Accessor
     */
    public String getSecret() {
        return secret;
    }

    /**
     * Replace the object being serialized with a proxy.
     * 
     * @return
     */
    private Object writeReplace() {
        return new SimpleProtectedSecretProxy(this);
    }

    /**
     * Serialize object. We throw an exception since this method should never be
     * called - the standard serialization engine will serialize the proxy
     * returned by writeReplace(). Anyone calling this method directly is
     * probably up to no good.
     * 
     * @param stream
     * @return
     * @throws InvalidObjectException
     */
    private void writeObject(ObjectOutputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }

    /**
     * Deserialize object. We throw an exception since this method should never
     * be called - the standard serialization engine will create serialized
     * proxies instead. Anyone calling this method directly is probably up to no
     * good and using a manually constructed serialized object.
     * 
     * @param stream
     * @return
     * @throws InvalidObjectException
     */
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }

    /**
     * Serializable proxy for our protected class. The encryption code is based
     * on https://gist.github.com/mping/3899247.
     */
    private static class SimpleProtectedSecretProxy implements Serializable {
        private static final long serialVersionUID = 1L;
        private String secret;

        private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
        private static final String HMAC_ALGORITHM = "HmacSHA256";

        private static transient SecretKeySpec cipherKey;
        private static transient SecretKeySpec hmacKey;

        static {
            // these keys can be read from the environment, the filesystem, etc.
            final byte[] aes_key = "d2cb415e067c7b13".getBytes();
            final byte[] hmac_key = "d6cfaad283353507".getBytes();

            try {
                cipherKey = new SecretKeySpec(aes_key, "AES");
                hmacKey = new SecretKeySpec(hmac_key, HMAC_ALGORITHM);
            } catch (Exception e) {
                throw new ExceptionInInitializerError(e);
            }
        }

        /**
         * Constructor.
         * 
         * @param protectedSecret
         */
        SimpleProtectedSecretProxy(ProtectedSecret protectedSecret) {
            this.secret = protectedSecret.secret;
        }

        /**
         * Write encrypted object to serialization stream.
         * 
         * @param s
         * @throws IOException
         */
        private void writeObject(ObjectOutputStream s) throws IOException {
            s.defaultWriteObject();
            try {
                Cipher encrypt = Cipher.getInstance(CIPHER_ALGORITHM);
                encrypt.init(Cipher.ENCRYPT_MODE, cipherKey);
                byte[] ciphertext = encrypt.doFinal(secret.getBytes("UTF-8"));
                byte[] iv = encrypt.getIV();

                Mac mac = Mac.getInstance(HMAC_ALGORITHM);
                mac.init(hmacKey);
                mac.update(iv);
                byte[] hmac = mac.doFinal(ciphertext);

                // TBD: write algorithm id...
                s.writeInt(iv.length);
                s.write(iv);
                s.writeInt(ciphertext.length);
                s.write(ciphertext);
                s.writeInt(hmac.length);
                s.write(hmac);
            } catch (Exception e) {
                throw new InvalidObjectException("unable to encrypt value");
            }
        }

        /**
         * Read encrypted object from serialization stream.
         * 
         * @param s
         * @throws InvalidObjectException
         */
        private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException, InvalidObjectException {
            s.defaultReadObject();
            try {
                // TBD: read algorithm id...
                byte[] iv = new byte[s.readInt()];
                s.read(iv);
                byte[] ciphertext = new byte[s.readInt()];
                s.read(ciphertext);
                byte[] hmac = new byte[s.readInt()];
                s.read(hmac);

                // verify HMAC
                Mac mac = Mac.getInstance(HMAC_ALGORITHM);
                mac.init(hmacKey);
                mac.update(iv);
                byte[] signature = mac.doFinal(ciphertext);

                // verify HMAC
                if (!Arrays.equals(hmac, signature)) {
                    throw new InvalidObjectException("unable to decrypt value");
                }

                // decrypt data
                Cipher decrypt = Cipher.getInstance(CIPHER_ALGORITHM);
                decrypt.init(Cipher.DECRYPT_MODE, cipherKey, new IvParameterSpec(iv));
                byte[] data = decrypt.doFinal(ciphertext);
                secret = new String(data, "UTF-8");
            } catch (Exception e) {
                throw new InvalidObjectException("unable to decrypt value");
            }
        }

        /**
         * Return protected object.
         * 
         * @return
         */
        private Object readResolve() {
            return new ProtectedSecret(secret);
        }
    }
}

毋庸置疑,加密密钥不应如图所示进行硬编码或缓存。 这是一条捷径,让我们可以专注于实施的细节。

密码和消息摘要应使用不同的密钥。 如果使用相同的密钥,则将严重损害系统的安全性。

任何生产系统都应处理另外两件事:密钥轮换以及更改密码和摘要算法。 前者可以通过在有效负载中添加“密钥ID”来处理,后者可以通过绑定序列化版本号和密码算法来处理。 例如,版本1使用标准AES,版本2使用AES-256。 解串器应该能够处理旧的加密密钥和密码(在合理范围内)。

测试码

测试代码很简单。 它创建一个对象,对其进行序列化,反序列化,然后将结果与原始值进行比较。

public class ProtectedSecretTest {

    /**
     * Test 'happy path'.
     */
    @Test
    public void testCipher() throws IOException, ClassNotFoundException {
        ProtectedSecret secret1 = new ProtectedSecret("password");
        ProtectedSecret secret2;
        byte[] ser;

        // serialize object
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutput output = new ObjectOutputStream(baos)) {
            output.writeObject(secret1);
            output.flush();

            ser = baos.toByteArray();
        }

        // deserialize object.
        try (ByteArrayInputStream bais = new ByteArrayInputStream(ser); ObjectInput input = new ObjectInputStream(bais)) {
            secret2 = (ProtectedSecret) input.readObject();
        }

        // compare values.
        assertEquals(secret1.getSecret(), secret2.getSecret());
    }

    /**
     * Test deserialization after a single bit is flipped.
     */
    @Test(expected = InvalidObjectException.class)
    public void testCipherAltered() throws IOException, ClassNotFoundException {
        ProtectedSecret secret1 = new ProtectedSecret("password");
        ProtectedSecret secret2;
        byte[] ser;

        // serialize object
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutput output = new ObjectOutputStream(baos)) {
            output.writeObject(secret1);
            output.flush();

            ser = baos.toByteArray();
        }
        
        // corrupt ciphertext
        ser[ser.length - 16 - 1 - 3] ^= 1;

        // deserialize object.
        try (ByteArrayInputStream bais = new ByteArrayInputStream(ser); ObjectInput input = new ObjectInputStream(bais)) {
            secret2 = (ProtectedSecret) input.readObject();
        }

        // compare values.
        assertEquals(secret1.getSecret(), secret2.getSecret());
    }
}

最后的话

我不能过分强调–这主要是一种智力活动。 像往常一样,最大的问题是密钥管理,而不是加密,并且通过前者所需的工作水平,您可能可以更快地实现更传统的解决方案。

在某些情况下,这可能仍然“足够好”。 例如,您可能只需要在长时间运行的应用程序期间保留数据。 在这种情况下,您可以在启动时创建随机密钥,并在程序结束后直接丢弃所有序列化的数据。

翻译自: https://www.javacodegeeks.com/2015/06/auto-encrypting-serializable-classes.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值