diff options
author | 2024-12-02 16:47:25 +0000 | |
---|---|---|
committer | 2024-12-02 16:47:25 +0000 | |
commit | 5e92696e00794b561ff4758db19d77ad852187b4 (patch) | |
tree | cad529f2fd38f44f3dc5d871c09a9716fe8ac64d | |
parent | 069fd890140bca348e469719c25b25c729680fb4 (diff) | |
parent | c948c80be52d333046b47610668b82113822301c (diff) |
Merge "ArrayUtils: add secure zeroization support" into main
-rw-r--r-- | core/java/com/android/internal/util/ArrayUtils.java | 45 | ||||
-rw-r--r-- | core/jni/Android.bp | 1 | ||||
-rw-r--r-- | core/jni/AndroidRuntime.cpp | 2 | ||||
-rw-r--r-- | core/jni/com_android_internal_util_ArrayUtils.cpp | 122 | ||||
-rw-r--r-- | core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java | 47 |
5 files changed, 217 insertions, 0 deletions
diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index 11123a9986f9..c257c5db6465 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -85,6 +85,51 @@ public class ArrayUtils { } /** + * 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. * * @param array1 the first byte array diff --git a/core/jni/Android.bp b/core/jni/Android.bp index f6689d585df1..82babe7a3160 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -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", diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index c5df248ec1a9..c005d63ff797 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -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), diff --git a/core/jni/com_android_internal_util_ArrayUtils.cpp b/core/jni/com_android_internal_util_ArrayUtils.cpp new file mode 100644 index 000000000000..c69e6c903ee5 --- /dev/null +++ b/core/jni/com_android_internal_util_ArrayUtils.cpp @@ -0,0 +1,122 @@ +/* + * 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 diff --git a/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java index fc233fba082e..f06adcedf645 100644 --- a/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java +++ b/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java @@ -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]); + } } |