2026-04-21 17:24:11 +02:00
|
|
|
package ai.nomyo;
|
|
|
|
|
|
|
|
|
|
import lombok.Getter;
|
|
|
|
|
import lombok.Setter;
|
|
|
|
|
|
2026-04-29 19:13:48 +02:00
|
|
|
import java.lang.foreign.*;
|
|
|
|
|
import java.lang.invoke.MethodHandle;
|
|
|
|
|
import java.nio.ByteBuffer;
|
|
|
|
|
import java.util.Arrays;
|
2026-04-21 17:24:11 +02:00
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-23 13:36:46 +02:00
|
|
|
* Cross-platform memory locking and secure zeroing for sensitive cryptographic buffers. Fails gracefully if unavailable.
|
2026-04-21 17:24:11 +02:00
|
|
|
*/
|
2026-04-29 16:59:33 +02:00
|
|
|
@SuppressWarnings("SameReturnValue")
|
2026-04-21 17:24:11 +02:00
|
|
|
public final class SecureMemory {
|
|
|
|
|
|
|
|
|
|
@Getter
|
|
|
|
|
private static final boolean HAS_MEMORY_LOCKING;
|
|
|
|
|
@Getter
|
|
|
|
|
private static final boolean HAS_SECURE_ZEROING;
|
2026-04-23 13:36:46 +02:00
|
|
|
@Getter
|
|
|
|
|
@Setter
|
|
|
|
|
private static volatile boolean secureMemoryEnabled = true;
|
2026-04-21 17:24:11 +02:00
|
|
|
|
2026-04-29 19:13:48 +02:00
|
|
|
private static final MethodHandle MLOCK_HANDLE;
|
|
|
|
|
private static final MethodHandle MUNLOCK_HANDLE;
|
|
|
|
|
|
2026-04-21 17:24:11 +02:00
|
|
|
static {
|
|
|
|
|
boolean locking = false;
|
|
|
|
|
boolean zeroing = false;
|
2026-04-29 19:13:48 +02:00
|
|
|
MethodHandle mlockHandle = null;
|
|
|
|
|
MethodHandle munlockHandle = null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
Linker linker = Linker.nativeLinker();
|
|
|
|
|
SymbolLookup stdLib = linker.defaultLookup();
|
|
|
|
|
|
|
|
|
|
var mlockOpt = stdLib.find("mlock");
|
|
|
|
|
var munlockOpt = stdLib.find("munlock");
|
|
|
|
|
|
|
|
|
|
if (mlockOpt.isPresent() && munlockOpt.isPresent()) {
|
|
|
|
|
MemorySegment mlockAddr = mlockOpt.get();
|
|
|
|
|
MemorySegment munlockAddr = munlockOpt.get();
|
|
|
|
|
FunctionDescriptor mlockDesc = FunctionDescriptor.of(
|
|
|
|
|
ValueLayout.JAVA_INT,
|
|
|
|
|
ValueLayout.ADDRESS,
|
|
|
|
|
ValueLayout.JAVA_LONG);
|
|
|
|
|
|
|
|
|
|
FunctionDescriptor munlockDesc = FunctionDescriptor.of(
|
|
|
|
|
ValueLayout.JAVA_INT,
|
|
|
|
|
ValueLayout.ADDRESS,
|
|
|
|
|
ValueLayout.JAVA_LONG);
|
|
|
|
|
|
|
|
|
|
mlockHandle = linker.downcallHandle(mlockAddr, mlockDesc);
|
|
|
|
|
munlockHandle = linker.downcallHandle(munlockAddr, munlockDesc);
|
|
|
|
|
|
|
|
|
|
locking = true;
|
|
|
|
|
}
|
|
|
|
|
} catch (Throwable t) {
|
|
|
|
|
// Degrade gracefully
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MLOCK_HANDLE = mlockHandle;
|
|
|
|
|
MUNLOCK_HANDLE = munlockHandle;
|
2026-04-23 13:36:46 +02:00
|
|
|
|
2026-04-21 17:24:11 +02:00
|
|
|
try {
|
2026-04-29 19:13:48 +02:00
|
|
|
locking = initMemoryLocking(locking);
|
2026-04-21 17:24:11 +02:00
|
|
|
zeroing = true; // Secure zeroing is always available at the JVM level
|
|
|
|
|
} catch (Throwable t) {
|
|
|
|
|
// Degrade gracefully
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
HAS_MEMORY_LOCKING = locking;
|
|
|
|
|
HAS_SECURE_ZEROING = zeroing;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 19:13:48 +02:00
|
|
|
private static boolean initMemoryLocking(boolean preCheck) {
|
|
|
|
|
if (MLOCK_HANDLE == null || MUNLOCK_HANDLE == null || !preCheck) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try (Arena arena = Arena.ofConfined()) {
|
|
|
|
|
MemorySegment testSegment = arena.allocate(1);
|
|
|
|
|
long result = (long) MLOCK_HANDLE.invokeExact(testSegment, (long) 1);
|
|
|
|
|
return result == 0;
|
|
|
|
|
} catch (Throwable t) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-04-21 17:24:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-23 13:36:46 +02:00
|
|
|
* Recommended way to handle sensitive data — use within try-with-resources for secure zeroing.
|
|
|
|
|
*/
|
|
|
|
|
public static SecureBuffer secureByteArray(byte[] data, boolean lock) {
|
|
|
|
|
return new SecureBuffer(data, lock);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Always attempts locking.
|
|
|
|
|
*/
|
|
|
|
|
public static SecureBuffer secureByteArray(byte[] data) {
|
|
|
|
|
return secureByteArray(data, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns protection capabilities: enabled, protection_level, has_memory_locking, has_secure_zeroing, supports_full_protection, page_size.
|
|
|
|
|
*/
|
|
|
|
|
public static Map<String, Object> getMemoryProtectionInfo() {
|
|
|
|
|
String protectionLevel;
|
|
|
|
|
if (HAS_MEMORY_LOCKING && HAS_SECURE_ZEROING) {
|
|
|
|
|
protectionLevel = "full";
|
|
|
|
|
} else if (HAS_SECURE_ZEROING) {
|
|
|
|
|
protectionLevel = "zeroing_only";
|
|
|
|
|
} else {
|
|
|
|
|
protectionLevel = "none";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
boolean supportsFull = HAS_MEMORY_LOCKING && HAS_SECURE_ZEROING && secureMemoryEnabled;
|
|
|
|
|
|
|
|
|
|
return Map.of("enabled", secureMemoryEnabled, "protection_level", protectionLevel, "has_memory_locking", HAS_MEMORY_LOCKING, "has_secure_zeroing", HAS_SECURE_ZEROING, "supports_full_protection", supportsFull, "page_size", Constants.PAGE_SIZE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Wraps bytes with memory locking and guaranteed zeroing on close. AutoCloseable for try-with-resources.
|
2026-04-21 17:24:11 +02:00
|
|
|
*/
|
2026-04-29 16:59:33 +02:00
|
|
|
@SuppressWarnings("SameReturnValue")
|
2026-04-21 17:24:11 +02:00
|
|
|
public static class SecureBuffer implements AutoCloseable {
|
|
|
|
|
|
|
|
|
|
private final Arena arena;
|
|
|
|
|
|
|
|
|
|
@Getter
|
|
|
|
|
private final MemorySegment data;
|
|
|
|
|
|
|
|
|
|
@Getter
|
|
|
|
|
private final long size;
|
|
|
|
|
|
|
|
|
|
@Getter
|
|
|
|
|
private final long address;
|
|
|
|
|
private boolean locked;
|
|
|
|
|
private boolean closed;
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-29 19:13:48 +02:00
|
|
|
* @param data byte array to wrap (zeroed after copy to off-heap memory)
|
2026-04-23 13:36:46 +02:00
|
|
|
* @param lock whether to attempt memory locking
|
2026-04-21 17:24:11 +02:00
|
|
|
*/
|
|
|
|
|
public SecureBuffer(byte[] data, boolean lock) {
|
|
|
|
|
this.arena = Arena.ofConfined();
|
|
|
|
|
this.data = data != null ? this.arena.allocate(data.length) : MemorySegment.NULL;
|
|
|
|
|
|
|
|
|
|
if (data != null) {
|
|
|
|
|
this.data.asByteBuffer().put(data);
|
2026-04-29 19:13:48 +02:00
|
|
|
Arrays.fill(data, (byte) 0);
|
2026-04-21 17:24:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.size = this.data.byteSize();
|
|
|
|
|
this.address = this.data.address();
|
|
|
|
|
|
|
|
|
|
this.locked = false;
|
|
|
|
|
this.closed = false;
|
|
|
|
|
|
|
|
|
|
if (lock && SecureMemory.secureMemoryEnabled) {
|
|
|
|
|
this.locked = lock();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-23 13:36:46 +02:00
|
|
|
* Locks buffer in memory (prevents disk swapping). Returns false if unavailable.
|
2026-04-21 17:24:11 +02:00
|
|
|
*/
|
|
|
|
|
public boolean lock() {
|
2026-04-29 19:13:48 +02:00
|
|
|
if (this.locked || this.address == 0) {
|
|
|
|
|
return this.locked;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
long result = (long) MLOCK_HANDLE.invokeExact(
|
|
|
|
|
MemorySegment.ofAddress(this.address),
|
|
|
|
|
this.size);
|
|
|
|
|
this.locked = result == 0;
|
|
|
|
|
} catch (Throwable t) {
|
|
|
|
|
this.locked = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.locked;
|
2026-04-21 17:24:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-23 13:36:46 +02:00
|
|
|
* Unlocks buffer (allows disk swapping).
|
2026-04-21 17:24:11 +02:00
|
|
|
*/
|
|
|
|
|
public boolean unlock() {
|
2026-04-29 19:13:48 +02:00
|
|
|
if (!locked || this.address == 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
long result = (long) MUNLOCK_HANDLE.invokeExact(
|
|
|
|
|
MemorySegment.ofAddress(this.address),
|
|
|
|
|
this.size);
|
|
|
|
|
locked = result != 0;
|
|
|
|
|
return result == 0;
|
|
|
|
|
} catch (Throwable t) {
|
|
|
|
|
locked = true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-04-21 17:24:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-23 13:36:46 +02:00
|
|
|
* Securely zeros buffer contents.
|
2026-04-21 17:24:11 +02:00
|
|
|
*/
|
|
|
|
|
public void zero() {
|
|
|
|
|
if (data != null) {
|
|
|
|
|
data.fill((byte) 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void close() {
|
|
|
|
|
if (closed) return;
|
|
|
|
|
|
|
|
|
|
zero();
|
|
|
|
|
unlock();
|
|
|
|
|
|
|
|
|
|
arena.close();
|
|
|
|
|
closed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|