How do I zero-ise a secret key in java?

2019-02-07 05:15发布

问题:

Is the following java code sufficient for clearing the secret key in memory (setting all of its byte value to 0)?

zerorize(SecretKey key)
{
    byte[] rawKey = key.getEncoded();
    Arrays.fill(rawKey, (byte) 0);
}

In other words, does the getEncoded method return a copy or reference to the actual key? If a copy is returned, then how can I clear the secret key as a security measure?

回答1:

Before trying to clear the key, you should check first if the implementation of the SecretKey interface also implements the javax.security.auth.Destroyable interface. If so, prefer that of course.



回答2:

getEncoded() seems to mostly return a clone of the key (from the Oracle 1.6 source of for instance javax.security.auth.kerberos):

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

hence wiping the return data does not erase all copies of the key from memory.

The only way to wipe the key from the SecretKey is to cast it to javax.security.auth.Destroyable if it implements the interface and invoke the destroy() method:

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

Strangely enough it seems that all Key implementation do not implement javax.security.auth.Destroyable. com.sun.crypto.provider.DESedeKey does not nor does javax.crypto.spec.SecretKeySpecused for AES. Both of these key implementations also clone the key in the getEncoded method. So it seems for these very common algorithms 3DES and AES we don't have a way to wipe the memory for the secret key?



回答3:

GetEncoded returns a copy of the secret key (so clearing that has no effect on the secret key data), and destroy by default throws DestroyFailedException which is worse than useless. It is also only available in 1.8+ so Android is out of luck. Here's a hack that uses introspection to (1) invoke destroy if available and does not throw an exception, otherwise (2) zero the key data and set the reference to 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);
    }
}

The way to use this is like

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

I use the Closeable interface because Destroyable is a 1.8+ feature only. This version works on 1.7+ and is pretty efficient (it does a trial destroy on one key to decide to ever use it again).



回答4:

I'm pretty sure that clearing rawKey will not affect the data in key.

I don't think there's a way in general to clear the data in a SecretKey. Specific implementation classes may provide for that, but I'm not aware of any that do. In Android, the risk of leaving the data uncleared is very low. Each app runs in its own process and its memory is not visible from outside.

I suppose there's an attack scenario where a root-priviledged process can take snapshots of memory and send them off to some supercomputer somewhere for analysis, hoping to discover someone's secret keys. But I've never heard of such an attack, and it strikes me as not competitive with other ways to gain access to a system. Is there a reason you are worried about this particular hypothetical vulnerability?



回答5:

Depending on the technology powering the garbage collector, any single object may be moved (i.e. copied) in physical memory at any time, so you cannot be sure that you will really destroy the key by zeroing an array -- assuming that you can access "the" array which holds the key, and not a copy thereof.

In shorter words: if your security model and context call for zeroing keys, then you should not use Java at all (or just about anything but C and assembly).



回答6:

Except primitive values, everything else in Java is always passed by reference, including arrays, so yes, you are clearing the given byte array correctly.

However, SecretKey class probably still holds data needed to generate that byte array, there including eventually another copy of the given byte array, so you should investigate how to clear that data as well.



回答7:

In other words, does the getEncoded method return a copy or reference to the actual key?

key.getEncoded() will return a reference to an array.

If the content of key is discarded when you do the Array.fill depends on whether or not the key is backed by the returned array. Given the documentation, it seems to me as if the encoding of the key is another representation of the key, i.e., that the key is not backed by the returned array.

It's easy to find out though. Try the following:

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

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

If the output is false, you know that the key is still stored in SecretKey.



回答8:

Taking a slightly different tack, once you have identified the correct area of memory to overwrite, you might want to do it more than once:

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);
}