Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c948c80b authored by Eric Biggers's avatar Eric Biggers
Browse files

ArrayUtils: add secure zeroization support

Add methods for allocating non-movable byte and char arrays, and methods
that make a best effort to securely zeroize byte and char arrays.

These methods will be used in code that handles the user's lockscreen
credential and the secrets derived or unlocked from it.  These methods
provide the needed low-level primitives, and when possible they will be
accessed through helper classes such as LockscreenCredential.

Bug: 320392352
Test: atest FrameworksUtilTests:ArrayUtilsTest
Change-Id: I51b195dd3a26e6c3074bf70a864b147eea7b18b9
parent c947e10e
Loading
Loading
Loading
Loading
+45 −0
Original line number Diff line number Diff line
@@ -84,6 +84,51 @@ public class ArrayUtils {
        return (T[])VMRuntime.getRuntime().newUnpaddedArray(clazz, minLen);
    }

    /**
     * This is like <code>new byte[length]</code>, but it allocates the array as non-movable. This
     * prevents copies of the data from being left on the Java heap as a result of heap compaction.
     * Use this when the array will contain sensitive data such as a password or cryptographic key
     * that needs to be wiped from memory when no longer needed. The owner of the array is still
     * responsible for the zeroization; {@link #zeroize(byte[])} should be used to do so.
     *
     * @param length the length of the array to allocate
     * @return the new array
     */
    public static byte[] newNonMovableByteArray(int length) {
        return (byte[]) VMRuntime.getRuntime().newNonMovableArray(byte.class, length);
    }

    /**
     * Like {@link #newNonMovableByteArray(int)}, but allocates a char array.
     *
     * @param length the length of the array to allocate
     * @return the new array
     */
    public static char[] newNonMovableCharArray(int length) {
        return (char[]) VMRuntime.getRuntime().newNonMovableArray(char.class, length);
    }

    /**
     * Zeroizes a byte array as securely as possible. Use this when the array contains sensitive
     * data such as a password or cryptographic key.
     * <p>
     * This zeroizes the array in a way that is guaranteed to not be optimized out by the compiler.
     * If supported by the architecture, it zeroizes the data not just in the L1 data cache but also
     * in other levels of the memory hierarchy up to and including main memory (but not above that).
     * <p>
     * This works on any <code>byte[]</code>, but to ensure that copies of the array aren't left on
     * the Java heap the array should have been allocated with {@link #newNonMovableByteArray(int)}.
     * Use on other arrays might also introduce performance anomalies.
     *
     * @param array the array to zeroize
     */
    public static native void zeroize(byte[] array);

    /**
     * Like {@link #zeroize(byte[])}, but for char arrays.
     */
    public static native void zeroize(char[] array);

    /**
     * Checks if the beginnings of two byte arrays are equal.
     *
+1 −0
Original line number Diff line number Diff line
@@ -90,6 +90,7 @@ cc_library_shared_for_libandroid_runtime {
        "android_view_VelocityTracker.cpp",
        "android_view_VerifiedKeyEvent.cpp",
        "android_view_VerifiedMotionEvent.cpp",
        "com_android_internal_util_ArrayUtils.cpp",
        "com_android_internal_util_VirtualRefBasePtr.cpp",
        "core_jni_helpers.cpp",
        ":deviceproductinfoconstants_aidl",
+2 −0
Original line number Diff line number Diff line
@@ -215,6 +215,7 @@ extern int register_com_android_internal_os_Zygote(JNIEnv *env);
extern int register_com_android_internal_os_ZygoteCommandBuffer(JNIEnv *env);
extern int register_com_android_internal_os_ZygoteInit(JNIEnv *env);
extern int register_com_android_internal_security_VerityUtils(JNIEnv* env);
extern int register_com_android_internal_util_ArrayUtils(JNIEnv* env);
extern int register_com_android_internal_util_VirtualRefBasePtr(JNIEnv *env);
extern int register_android_window_WindowInfosListener(JNIEnv* env);
extern int register_android_window_ScreenCapture(JNIEnv* env);
@@ -1613,6 +1614,7 @@ static const RegJNIRec gRegJNI[] = {
        REG_JNI(register_com_android_internal_os_ZygoteCommandBuffer),
        REG_JNI(register_com_android_internal_os_ZygoteInit),
        REG_JNI(register_com_android_internal_security_VerityUtils),
        REG_JNI(register_com_android_internal_util_ArrayUtils),
        REG_JNI(register_com_android_internal_util_VirtualRefBasePtr),
        REG_JNI(register_android_hardware_Camera),
        REG_JNI(register_android_hardware_camera2_CameraMetadata),
+122 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define LOG_TAG "ArrayUtils"

#include <android-base/logging.h>
#include <jni.h>
#include <nativehelper/JNIHelp.h>
#include <string.h>
#include <unistd.h>
#include <utils/Log.h>

namespace android {

static size_t GetCacheLineSize() {
    long size = sysconf(_SC_LEVEL1_DCACHE_LINESIZE);
    if (size <= 0) {
        ALOGE("Unable to determine L1 data cache line size. Assuming 32 bytes");
        return 32;
    }
    return size;
}

#ifdef __aarch64__
static void CleanDataCache(const uint8_t* p, size_t size, size_t cache_line_size) {
    // Execute 'dc cvac' at least once on each cache line in the memory region.
    //
    // 'dc cvac' stands for "Data Cache line Clean by Virtual Address to point-of-Coherency".
    // It writes the cache line back to the "point-of-coherency", i.e. main memory.
    //
    // Since the memory region is not guaranteed to be cache-line-aligned, we use an "extra"
    // instruction after the loop to make sure the last cache line gets covered.
    for (size_t i = 0; i < size; i += cache_line_size) {
        asm volatile("dc cvac, %0" ::"r"(p + i));
    }
    asm volatile("dc cvac, %0" ::"r"(p + size - 1));
}
#elif defined(__i386__) || defined(__x86_64__)
static void CleanDataCache(const uint8_t* p, size_t size, size_t cache_line_size) {
    for (size_t i = 0; i < size; i += cache_line_size) {
        asm volatile("clflush (%0)" ::"r"(p + i));
    }
    asm volatile("clflush (%0)" ::"r"(p + size - 1));
}
#elif defined(__riscv)
static void CleanDataCache(const uint8_t* p, size_t size, size_t cache_line_size) {
    // This should eventually work, but it is not ready to be enabled yet:
    //  1.) The Android emulator needs to add support for zicbom.
    //  2.) Kernel needs to enable zicbom in usermode.
    //  3.) Android clang needs to add zicbom to the target.
#if 0
    for (size_t i = 0; i < size; i += cache_line_size) {
        asm volatile("cbo.clean (%0)" ::"r"(p + i));
    }
    asm volatile("cbo.clean (%0)" ::"r"(p + size - 1));
#endif
}
#elif defined(__arm__)
// arm32 has a cacheflush() syscall, but it is undocumented and only flushes the icache.
// It is not the same as cacheflush(2) as documented in the Linux man-pages project.
static void CleanDataCache(const uint8_t* p, size_t size, size_t cache_line_size) {}
#else
#error "Unknown architecture"
#endif

static void ZeroizePrimitiveArray(JNIEnv* env, jclass clazz, jarray array, size_t component_len) {
    static const size_t cache_line_size = GetCacheLineSize();

    size_t size = env->GetArrayLength(array) * component_len;
    if (size == 0) {
        return;
    }

    // ART guarantees that GetPrimitiveArrayCritical never copies.
    jboolean isCopy;
    void* elems = env->GetPrimitiveArrayCritical(array, &isCopy);
    CHECK(!isCopy);

#ifdef __BIONIC__
    memset_explicit(elems, 0, size);
#else
    memset(elems, 0, size);
#endif
    // Clean the data cache so that the data gets zeroized in main memory right away.  Without this,
    // it might not be written to main memory until the cache line happens to be evicted.
    CleanDataCache(static_cast<const uint8_t*>(elems), size, cache_line_size);

    env->ReleasePrimitiveArrayCritical(array, elems, /* mode= */ 0);
}

static void ZeroizeByteArray(JNIEnv* env, jclass clazz, jbyteArray array) {
    ZeroizePrimitiveArray(env, clazz, array, sizeof(jbyte));
}

static void ZeroizeCharArray(JNIEnv* env, jclass clazz, jcharArray array) {
    ZeroizePrimitiveArray(env, clazz, array, sizeof(jchar));
}

static const JNINativeMethod sMethods[] = {
        {"zeroize", "([B)V", (void*)ZeroizeByteArray},
        {"zeroize", "([C)V", (void*)ZeroizeCharArray},
};

int register_com_android_internal_util_ArrayUtils(JNIEnv* env) {
    return jniRegisterNativeMethods(env, "com/android/internal/util/ArrayUtils", sMethods,
                                    NELEM(sMethods));
}

} // namespace android
+47 −0
Original line number Diff line number Diff line
@@ -496,4 +496,51 @@ public class ArrayUtilsTest {
            // expected
        }
    }

    // Note: the zeroize() tests only test the behavior that can be tested from a Java test.
    // They do not verify that no copy of the data is left anywhere.

    @Test
    @SmallTest
    public void testZeroizeNonMovableByteArray() {
        final int length = 10;
        byte[] array = ArrayUtils.newNonMovableByteArray(length);
        assertArrayEquals(array, new byte[length]);
        Arrays.fill(array, (byte) 0xff);
        ArrayUtils.zeroize(array);
        assertArrayEquals(array, new byte[length]);
    }

    @Test
    @SmallTest
    public void testZeroizeRegularByteArray() {
        final int length = 10;
        byte[] array = new byte[length];
        assertArrayEquals(array, new byte[length]);
        Arrays.fill(array, (byte) 0xff);
        ArrayUtils.zeroize(array);
        assertArrayEquals(array, new byte[length]);
    }

    @Test
    @SmallTest
    public void testZeroizeNonMovableCharArray() {
        final int length = 10;
        char[] array = ArrayUtils.newNonMovableCharArray(length);
        assertArrayEquals(array, new char[length]);
        Arrays.fill(array, (char) 0xff);
        ArrayUtils.zeroize(array);
        assertArrayEquals(array, new char[length]);
    }

    @Test
    @SmallTest
    public void testZeroizeRegularCharArray() {
        final int length = 10;
        char[] array = new char[length];
        assertArrayEquals(array, new char[length]);
        Arrays.fill(array, (char) 0xff);
        ArrayUtils.zeroize(array);
        assertArrayEquals(array, new char[length]);
    }
}