/** * Node.js secure memory implementation. * * When the optional native addon (nomyo-native) is installed and built, * this implementation provides true OS-level memory locking (mlock) and * secure zeroing via explicit_bzero / SecureZeroMemory. * * Without the native addon it falls back to pure-JS zeroing only. * * To build the native addon: * cd native && npm install && npm run build */ import { SecureMemory } from './secure'; import { ProtectionInfo } from '../../types/crypto'; interface NativeAddon { mlockBuffer(buf: Buffer): boolean; munlockBuffer(buf: Buffer): boolean; secureZeroBuffer(buf: Buffer): void; getPageSize(): number; } // Try to load the optional native addon once at module init time. let nativeAddon: NativeAddon | null = null; try { // eslint-disable-next-line @typescript-eslint/no-var-requires const loaded = require('nomyo-native') as NativeAddon | null; if (loaded && typeof loaded.mlockBuffer === 'function') { nativeAddon = loaded; } } catch (_e) { // Native addon not installed — degrade gracefully } /** * Whether mlock actually succeeds in this process, determined once by trying it. * * Having the addon loaded is not the same as being allowed to lock: mlock is * routinely refused by RLIMIT_MEMLOCK (often only a few MB, and 0 in some * containers) without any elevated privileges. Reporting the capability rather * than the reality would overstate the protection actually in force. */ let lockProbeResult: boolean | null = null; function canActuallyLock(): boolean { if (lockProbeResult !== null) return lockProbeResult; if (nativeAddon === null) { lockProbeResult = false; return false; } try { const probe = Buffer.alloc(nativeAddon.getPageSize()); const locked = nativeAddon.mlockBuffer(probe); if (locked) { nativeAddon.munlockBuffer(probe); } lockProbeResult = locked; } catch (_e) { lockProbeResult = false; } return lockProbeResult; } export class NodeSecureMemory implements SecureMemory { private readonly hasNative: boolean; constructor() { // Deliberately silent: a library must not write to stdout on construction. // Callers surface this via getProtectionInfo() under their own debug flag. this.hasNative = nativeAddon !== null; } /** * Zero memory. * With native addon: uses explicit_bzero / SecureZeroMemory (not optimized away). * Without native addon: fills ArrayBuffer with zeros via Uint8Array (best effort). */ zeroMemory(data: ArrayBuffer): void { if (this.hasNative && nativeAddon) { // Buffer.from(arrayBuffer) shares the underlying memory (no copy) const buf = Buffer.from(data); nativeAddon.secureZeroBuffer(buf); } // Always also zero via JS for defence-in-depth const view = new Uint8Array(data); view.fill(0); } /** * Attempt to lock an ArrayBuffer in physical memory (prevent swapping). * Best-effort: fails gracefully without CAP_IPC_LOCK / elevated privileges. */ lockMemory(data: ArrayBuffer): boolean { if (!this.hasNative || !nativeAddon) return false; const buf = Buffer.from(data); return nativeAddon.mlockBuffer(buf); } /** * Unlock previously locked memory. */ unlockMemory(data: ArrayBuffer): boolean { if (!this.hasNative || !nativeAddon) return false; const buf = Buffer.from(data); return nativeAddon.munlockBuffer(buf); } /** * Get memory protection information. * * Reports what is actually in force, not what is theoretically supported — * `canLock` reflects a real mlock attempt. */ getProtectionInfo(): ProtectionInfo { if (!this.hasNative) { return { canLock: false, isPlatformSecure: false, method: 'zero-only', platform: process.platform, hasSecureZeroing: false, details: 'Node.js (pure JS): memory locking not available without the native addon. ' + 'Using best-effort zeroing only. ' + 'Build the optional addon for mlock support: cd native && npm install && npm run build', }; } if (!canActuallyLock()) { return { canLock: false, isPlatformSecure: false, method: 'zero-only', platform: process.platform, hasSecureZeroing: true, pageSize: nativeAddon!.getPageSize(), details: 'Node.js with nomyo-native addon: secure zeroing available, but mlock was ' + 'refused by the OS (typically RLIMIT_MEMLOCK). Sensitive buffers may still ' + 'be swapped to disk. Raise the memlock limit or grant CAP_IPC_LOCK to enable locking.', }; } return { canLock: true, isPlatformSecure: true, method: 'mlock', platform: process.platform, hasSecureZeroing: true, pageSize: nativeAddon!.getPageSize(), details: 'Node.js with nomyo-native addon: mlock + explicit_bzero/SecureZeroMemory active.', }; } }