以下 java 代码是否足以清除内存中的密钥(将其所有字节值设置为 0)?
zerorize(SecretKey key)
{
byte[] rawKey = key.getEncoded();
Arrays.fill(rawKey, (byte) 0);
}
换句话说,该getEncoded
方法是否返回对实际键的副本或引用?如果返回副本,那么如何清除密钥作为安全措施?
以下 java 代码是否足以清除内存中的密钥(将其所有字节值设置为 0)?
zerorize(SecretKey key)
{
byte[] rawKey = key.getEncoded();
Arrays.fill(rawKey, (byte) 0);
}
换句话说,该getEncoded
方法是否返回对实际键的副本或引用?如果返回副本,那么如何清除密钥作为安全措施?
在尝试清除密钥之前,您应该首先检查接口的实现是否SecretKey
也实现了javax.security.auth.Destroyable
接口。如果是这样,当然更喜欢。
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.Destroyable
。com.sun.crypto.provider.DESedeKey
既不也不javax.crypto.spec.SecretKeySpec
用于 AES。这两个密钥实现也克隆了getEncoded
方法中的密钥。因此,对于这些非常常见的算法 3DES 和 AES,我们似乎没有办法擦除密钥的内存?
GetEncoded 返回密钥的副本(因此清除对密钥数据没有影响),并且默认情况下destroy会抛出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+ 上工作并且非常高效(它对一个键进行试验破坏以决定再次使用它)。
我很确定清除rawKey
不会影响key
.
我认为一般没有办法清除 SecretKey 中的数据。特定的实现类可能会提供这一点,但我不知道有什么。在 Android 中,不清除数据的风险非常低。每个应用程序都在自己的进程中运行,并且它的内存从外部是不可见的。
我想有一个攻击场景,一个 root 特权进程可以拍摄内存快照并将它们发送到某处的某个超级计算机进行分析,希望能发现某人的密钥。但我从未听说过这样的攻击,我觉得它与其他获取系统访问权限的方式相比没有竞争力。您是否有理由担心这种特殊的假设漏洞?
根据为垃圾收集器提供动力的技术,任何单个对象都可能随时在物理内存中移动(即复制),因此您无法确定是否会通过将数组归零来真正破坏密钥——假设您可以访问“保存密钥的"数组,而不是其副本。
简而言之:如果您的安全模型和上下文要求归零键,那么您根本不应该使用 Java(或者除了 C 和汇编之外的任何东西)。
换句话说,getEncoded 方法是否返回对实际键的副本或引用?
key.getEncoded()
将返回对数组的引用。
执行 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
.
除了原始值之外,Java 中的所有其他内容始终通过引用传递,包括数组,所以是的,您正在正确清除给定的字节数组。
但是,SecretKey 类可能仍然保存生成该字节数组所需的数据,最终包括给定字节数组的另一个副本,因此您还应该研究如何清除该数据。
采取稍微不同的策略,一旦您确定了要覆盖的正确内存区域,您可能需要多次执行此操作:
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);
}