如何在java中将密钥归零?

Posted

技术标签:

【中文标题】如何在java中将密钥归零?【英文标题】:How do I zero-ise a secret key in java? 【发布时间】:2011-11-03 17:58:48 【问题描述】:

以下java代码是否足以清除内存中的密钥(将其所有字节值设置为0)?

zerorize(SecretKey key)

    byte[] rawKey = key.getEncoded();
    Arrays.fill(rawKey, (byte) 0);

换句话说,getEncoded 方法是否返回对实际键的副本或引用?如果返回副本,作为安全措施,如何清除密钥?

【问题讨论】:

【参考方案1】:

在尝试清除密钥之前,您应该首先检查SecretKey 接口的实现是否也实现了javax.security.auth.Destroyable 接口。如果是这样,当然更喜欢。

【讨论】:

仅适用于 1.8+,通常只会抛出 DestroyFailedException【参考方案2】:

getEncoded() 似乎主要返回密钥的克隆(来自 Oracle 1.6 源,例如 javax.security.auth.kerberos):

public final byte[] getEncoded() 
  if (destroyed)
    throw new IllegalStateException("This key is no longer valid");
  return (byte[])keyBytes.clone();

因此擦除返回数据不会从内存中删除密钥的所有副本。

SecretKey 中擦除密钥的唯一方法是将其强制转换为javax.security.auth.Destroyable,如果它实现了接口并调用destroy() 方法:

public void destroy() throws DestroyFailedException 
  if (!destroyed) 
    destroyed = true;
    Arrays.fill(keyBytes, (byte) 0);
  

奇怪的是,似乎所有的 Key 实现都没有实现javax.security.auth.Destroyablecom.sun.crypto.provider.DESedeKey 既没有也没有 javax.crypto.spec.SecretKeySpec 用于 AES。这两个密钥实现也克隆了 getEncoded 方法中的密钥。因此,对于这些非常常见的算法 3DES 和 AES,我们似乎没有办法擦除密钥的内存?

【讨论】:

【参考方案3】:

GetEncoded 返回密钥的副本(因此清除对密钥数据没有影响),并且默认情况下销毁会抛出 DestroyFailedException,这比无用更糟糕。它也仅在 1.8+ 中可用,因此 android 不走运。这是一个 hack,它使用自省 (1) 如果可用则调用 destroy 并且不抛出异常,否则 (2) 将关键数据归零并将引用设置为 null。

package kiss.cipher;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

import javax.crypto.spec.SecretKeySpec;

/**
 * Created by wmacevoy on 10/12/16.
 */
public class CloseableKey implements AutoCloseable 

    // forward portable to JDK 1.8 to destroy keys
    // but usable in older JDK's
    static final Method DESTROY;
    static final Field KEY;

    static 
        Method _destroy = null;

        Field _key = null;
        try 
            Method destroy = SecretKeySpec.class.getMethod("destroy");
            SecretKeySpec key = new SecretKeySpec(new byte[16], "AES");
            destroy.invoke(key);
            _destroy = destroy;
         catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) 
        

        try 
            _key = SecretKeySpec.class.getDeclaredField("key");
            _key.setAccessible(true);
         catch (NoSuchFieldException | SecurityException ex) 
        

        DESTROY = _destroy;
        KEY = _key;
    

    static void close(SecretKeySpec secretKeySpec) 
        if (secretKeySpec != null) 
            if (DESTROY != null) 
                try 
                    DESTROY.invoke(secretKeySpec);
                 catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) 
                    throw new IllegalStateException("inconceivable: " + ex);
                
             else if (KEY != null) 
                try 
                    byte[] key = (byte[]) KEY.get(secretKeySpec);
                    Arrays.fill(key, (byte) 0);
                    KEY.set(secretKeySpec, null);
                 catch (IllegalAccessException | IllegalArgumentException ex) 
                    throw new IllegalStateException("inconceivable: " + ex);
                
            
        
    

    public final SecretKeySpec secretKeySpec;

    CloseableKey(SecretKeySpec _secretKeySpec) 

        secretKeySpec = _secretKeySpec;
    

    @Override
    public void close() 
        close(secretKeySpec);
    

这个的使用方法是这样的

try (CloseableKey key = 
       new CloseableKey(new SecretKeySpec(data, 0, 16, "AES"))) 
  aesecb.init(Cipher.ENCRYPT_MODE, key.secretKeySpec);

我使用 Closeable 界面是因为 Destroyable 只是 1.8+ 的功能。这个版本在 1.7+ 上运行并且非常高效(它会在一个键上进行试验销毁以决定再次使用它)。

【讨论】:

这是一个 hack,GC 可以重新打包内存或操作系统将数据移动到交换,这可能会泄漏关键数据。尽快关闭密钥,以尽量减少由于 GC 或 OS 副作用而泄漏的可能性。【参考方案4】:

我很确定清除rawKey 不会影响key 中的数据。

我认为一般来说没有办法清除 SecretKey 中的数据。特定的实现类可能提供了这一点,但我不知道有什么这样做的。在 Android 中,不清除数据的风险非常低。每个应用程序都在自己的进程中运行,其内存从外部是不可见的。

我想有一个攻击场景,root 特权进程可以拍摄内存快照并将它们发送到某处的某个超级计算机进行分析,希望能发现某人的密钥。但我从未听说过这样的攻击,我觉得它与其他获取系统访问权限的方式相比没有竞争力。您是否有理由担心这个特定的假设漏洞?

【讨论】:

【参考方案5】:

根据支持垃圾收集器的技术,任何单个对象都可能随时在物理内存中移动(即复制),因此您无法确定是否真的会通过清零数组来破坏密钥——假设您可以访问保存密钥的“the”数组,而不是其副本。

简而言之:如果您的安全模型和上下文要求归零密钥,那么您根本不应该使用 Java(或者除了 C 和汇编之外的任何东西)。

【讨论】:

但是如果您必须使用 Java,请在 GC 可能重新打包数据或操作系统将其移动到交换等之前快速将其归零。【参考方案6】:

换句话说,getEncoded 方法是否返回对实际密钥的副本或引用?

key.getEncoded() 将返回一个 reference 到一个数组。

如果在执行 Array.fill 时丢弃 key 的内容,取决于返回的数组是否 支持 key。鉴于文档,在我看来,密钥的 编码 似乎是密钥的另一种表示,即密钥 由返回的数组支持。

虽然很容易找到。请尝试以下操作:

byte[] rawKey = key.getEncoded();
Arrays.fill(rawKey, (byte) 0);

byte[] again = key.getEncoded();
Log.d(Arrays.equals(rawKey, again));

如果输出为false,则知道密钥仍存储在SecretKey中。

【讨论】:

【参考方案7】:

除了原始值,Java 中的所有其他内容总是通过引用传递,包括数组,所以是的,您正在正确清除给定的字节数组。

但是,SecretKey 类可能仍然保存生成该字节数组所需的数据,其中最终包括给定字节数组的另一个副本,因此您还应该研究如何清除该数据。

【讨论】:

-1: Java 中的所有其他内容总是通过引用传递 -- 不,Java 是总是 通过值传递!你不能通过值传递一个对象的原因是因为没有变量可以包含一个对象! @aioobe .. 你确定我们说的是同一个 Java 吗? int 是传值,boolean 是传值,Integer 是引用,以及任何对象、数组等... Java 传递“一个值”,其实就是对一个对象的“引用”,所以它是通过参考。 @SimoneGianni:请忽略我之前的评论,我脑残了。但 aioobe 是对的:通过值传递引用与通过引用传递某些东西不同 @aioobe:你的评论有点误导。事实上,在 Java 中,所有东西都按值传递,包括原始类型和对象引用。事实上,变量只能保存对对象的引用,而不是对象本身。但是如果没有an explanation,那会很混乱。 @Simone - 知道它是参考对 OP 没有帮助。问题是,对 what 的引用?特别是,它是对密钥内部数据的引用还是对数据副本的引用? OP想知道清除数组是否会清除密钥中的敏感数据。【参考方案8】:

采取稍微不同的策略,一旦您确定了要覆盖的正确内存区域,您可能需要多次执行此操作:

zerorize(SecretKey key)

    byte[] rawKey = key.getEncoded();
    Arrays.fill(rawKey, (byte) 0xFF);
    Arrays.fill(rawKey, (byte) 0xAA);
    Arrays.fill(rawKey, (byte) 0x55);
    Arrays.fill(rawKey, (byte) 0x00);

【讨论】:

如上图,key.getEncoded(); 只返回一个副本,所以你在这里删除nil..

以上是关于如何在java中将密钥归零?的主要内容,如果未能解决你的问题,请参考以下文章

在C代码中将数组归零[重复]

在 C 中将二维数组归零的最快方法是啥?

在c中将结构的内存区域归零[重复]

如何在 Vue 中将 API 应用程序密钥作为道具传递

如何快速将数组归零?

如何在 J2ME 中将数据发布到服务器?从服务器获取响应无效的 API 密钥