UNPKG

wallet-storage-client

Version:
296 lines 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PrivilegedKeyManager = void 0; const sdk_1 = require("@bsv/sdk"); /** * PrivilegedKeyManager * * This class manages a privileged (i.e., very sensitive) private key, obtained from * an external function (`keyGetter`), which might be backed by HSMs, secure enclaves, * or other secure storage. The manager retains the key in memory only for a limited * duration (`retentionPeriod`), uses XOR-based chunk-splitting obfuscation, and * includes decoy data to raise the difficulty of discovering the real key in memory. * * IMPORTANT: While these measures raise the bar for attackers, JavaScript environments * do not provide perfect in-memory secrecy. */ class PrivilegedKeyManager { /** * @param keyGetter - Asynchronous function that retrieves the PrivateKey from a secure environment. * @param retentionPeriod - Time in milliseconds to retain the obfuscated key in memory before zeroizing. */ constructor(keyGetter, retentionPeriod = 120000) { /** * A list of dynamically generated property names used to store * real key chunks (XORed with random pads). */ this.chunkPropNames = []; /** * A list of dynamically generated property names used to store * the random pads that correspond to the real key chunks. */ this.chunkPadPropNames = []; /** * A list of decoy property names that will be removed * when the real key is destroyed. */ this.decoyPropNamesDestroy = []; /** * A list of decoy property names that remain in memory * even after the real key is destroyed (just to cause confusion). */ this.decoyPropNamesRemain = []; /** * Number of chunks to split the 32-byte key into. * Adjust to increase or decrease obfuscation complexity. */ this.CHUNK_COUNT = 4; this.keyGetter = keyGetter; this.retentionPeriod = retentionPeriod; // Initialize some random decoy properties that always remain: for (let i = 0; i < 2; i++) { const propName = this.generateRandomPropName(); // Store random garbage to cause confusion this[propName] = Uint8Array.from((0, sdk_1.Random)(16)); this.decoyPropNamesRemain.push(propName); } } /** * Safely destroys the in-memory obfuscated key material by zeroizing * and deleting related fields. Also destroys some (but not all) decoy * properties to further confuse an attacker. */ destroyKey() { try { // Zero out real chunk data for (const name of this.chunkPropNames) { const data = this[name]; if (data) { data.fill(0); } delete this[name]; } for (const name of this.chunkPadPropNames) { const data = this[name]; if (data) { data.fill(0); } delete this[name]; } // Destroy some decoys for (const name of this.decoyPropNamesDestroy) { const data = this[name]; if (data) { data.fill(0); } delete this[name]; } // Clear arrays of property names this.chunkPropNames = []; this.chunkPadPropNames = []; this.decoyPropNamesDestroy = []; } catch (_) { // Swallow any errors in the destruction process } finally { if (this.destroyTimer) { clearTimeout(this.destroyTimer); this.destroyTimer = undefined; } } } /** * Re/sets the destruction timer that removes the key from memory * after `retentionPeriod` ms. If a timer is already running, it * is cleared and re-set. This ensures the key remains in memory * for exactly the desired window after its most recent acquisition. */ scheduleKeyDestruction() { if (this.destroyTimer) { // TODO: Consider a constructor flag to avoid clearing timers for higher security clearTimeout(this.destroyTimer); } this.destroyTimer = setTimeout(() => { this.destroyKey(); }, this.retentionPeriod); } /** * XOR-based obfuscation on a per-chunk basis. * This function takes two equal-length byte arrays * and returns the XOR combination. */ xorBytes(a, b) { const out = new Uint8Array(a.length); for (let i = 0; i < a.length; i++) { out[i] = a[i] ^ b[i]; } return out; } /** * Splits the 32-byte key into `this.CHUNK_COUNT` smaller chunks * (mostly equal length; the last chunk picks up leftover bytes * if 32 is not evenly divisible). */ splitKeyIntoChunks(keyBytes) { const chunkSize = Math.floor(keyBytes.length / this.CHUNK_COUNT); const chunks = []; let offset = 0; for (let i = 0; i < this.CHUNK_COUNT; i++) { const size = (i === this.CHUNK_COUNT - 1) ? keyBytes.length - offset : chunkSize; chunks.push(keyBytes.slice(offset, offset + size)); offset += size; } return chunks; } /** * Reassembles the chunks from the dynamic properties, XORs them * with their corresponding pads, and returns a single 32-byte * Uint8Array representing the raw key. */ reassembleKeyFromChunks() { try { const chunkArrays = []; for (let i = 0; i < this.chunkPropNames.length; i++) { const chunkEnc = this[this.chunkPropNames[i]]; const chunkPad = this[this.chunkPadPropNames[i]]; if (!chunkEnc || !chunkPad || chunkEnc.length !== chunkPad.length) { return null; } const rawChunk = this.xorBytes(chunkEnc, chunkPad); chunkArrays.push(rawChunk); } // Concat them back to a single 32-byte array: const totalLength = chunkArrays.reduce((sum, c) => sum + c.length, 0); if (totalLength !== 32) { // We only handle 32-byte keys return null; } const rawKey = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunkArrays) { rawKey.set(chunk, offset); offset += chunk.length; // Attempt to zero the ephemeral chunk chunk.fill(0); } return rawKey; } catch (_) { // If any property is missing or type mismatch, we return null return null; } } /** * Generates a random property name to store key chunks or decoy data. */ generateRandomPropName() { // E.g., 8 random hex characters for the property name const randomHex = sdk_1.Utils.toHex((0, sdk_1.Random)(4)); return `_${randomHex}_${Math.floor(Math.random() * 1e6)}`; } /** * Forces a PrivateKey to be represented as exactly 32 bytes, left-padding * with zeros if its numeric value has fewer than 32 bytes. */ get32ByteRepresentation(privKey) { // The internal "toArray()" can be up to 32 bytes, but sometimes fewer // if the numeric value has leading zeros. const buf = privKey.toArray(); if (buf.length > 32) { throw new Error('PrivilegedKeyManager: Expected a 32-byte key, but got more.'); } // Left-pad with zeros if needed const keyBytes = new Uint8Array(32); keyBytes.set(buf, 32 - buf.length); return keyBytes; } /** * Returns the privileged key needed to perform cryptographic operations. * Uses in-memory chunk-based obfuscation if the key was already fetched. * Otherwise, it calls out to `keyGetter`, splits the 32-byte representation * of the key, XORs each chunk with a random pad, and stores them under * dynamic property names. Also populates new decoy properties. * * @param reason - The reason for why the key is needed, passed to keyGetter. * @returns The PrivateKey object needed for cryptographic operations. */ async getPrivilegedKey(reason) { // If we already have chunk properties, try reassemble if (this.chunkPropNames.length > 0 && this.chunkPadPropNames.length > 0) { const rawKeyBytes = this.reassembleKeyFromChunks(); if (rawKeyBytes && rawKeyBytes.length === 32) { // Convert 32 raw bytes back to a PrivateKey // (Leading zeros are preserved, but PrivateKey() will parse it as a big integer.) const hexKey = sdk_1.Utils.toHex([...rawKeyBytes]); // 64 hex chars rawKeyBytes.fill(0); // Zero ephemeral copy this.scheduleKeyDestruction(); return new sdk_1.PrivateKey(hexKey, 'hex'); } } // Otherwise, fetch a fresh key from the secure environment const fetchedKey = await this.keyGetter(reason); // Force 32‑byte representation (left-pad if necessary) const keyBytes = this.get32ByteRepresentation(fetchedKey); // Clean up any old data first (in case we had something stale) this.destroyKey(); // Split the key const chunks = this.splitKeyIntoChunks(keyBytes); // Store new chunk data under random property names for (let i = 0; i < chunks.length; i++) { const chunkProp = this.generateRandomPropName(); const padProp = this.generateRandomPropName(); this.chunkPropNames.push(chunkProp); this.chunkPadPropNames.push(padProp); // Generate random pad of the same length as the chunk const pad = Uint8Array.from((0, sdk_1.Random)(chunks[i].length)); // XOR the chunk to obfuscate const obf = this.xorBytes(chunks[i], pad); // Store them in dynamic properties this[chunkProp] = obf; this[padProp] = pad; } // Generate some decoy properties that will be destroyed with the key for (let i = 0; i < 2; i++) { const decoyProp = this.generateRandomPropName(); this[decoyProp] = Uint8Array.from((0, sdk_1.Random)(32)); this.decoyPropNamesDestroy.push(decoyProp); } // Zero out ephemeral original keyBytes.fill(0); // Schedule destruction this.scheduleKeyDestruction(); // Return the newly fetched key as a normal PrivateKey return fetchedKey; } async getPublicKey(args) { return new sdk_1.ProtoWallet(await this.getPrivilegedKey(args.privilegedReason)).getPublicKey(args); } async revealCounterpartyKeyLinkage(args) { return new sdk_1.ProtoWallet(await this.getPrivilegedKey(args.privilegedReason)).revealCounterpartyKeyLinkage(args); } async revealSpecificKeyLinkage(args) { return new sdk_1.ProtoWallet(await this.getPrivilegedKey(args.privilegedReason)).revealSpecificKeyLinkage(args); } async encrypt(args) { return new sdk_1.ProtoWallet(await this.getPrivilegedKey(args.privilegedReason)).encrypt(args); } async decrypt(args) { return new sdk_1.ProtoWallet(await this.getPrivilegedKey(args.privilegedReason)).decrypt(args); } async createHmac(args) { return new sdk_1.ProtoWallet(await this.getPrivilegedKey(args.privilegedReason)).createHmac(args); } async verifyHmac(args) { return new sdk_1.ProtoWallet(await this.getPrivilegedKey(args.privilegedReason)).verifyHmac(args); } async createSignature(args) { return new sdk_1.ProtoWallet(await this.getPrivilegedKey(args.privilegedReason)).createSignature(args); } async verifySignature(args) { return new sdk_1.ProtoWallet(await this.getPrivilegedKey(args.privilegedReason)).verifySignature(args); } } exports.PrivilegedKeyManager = PrivilegedKeyManager; //# sourceMappingURL=PrivilegedKeyManager.js.map