The secure key utilities provide defense-in-depth protection for private key handling through memory zeroing, scoped access patterns, and automatic cleanup. These utilities ensure that sensitive cryptographic material is handled safely and cleared from memory immediately after use.
Security model
The secure key system implements multiple layers of protection:
- Memory zeroing - Overwrites key material before releasing references
- Scoped key access - Keys are only available within controlled callbacks
- Automatic cleanup - Keys are cleared after use via try/finally patterns
- No persistence - Keys are never stored to disk or localStorage
While JavaScript doesn’t guarantee immediate memory clearing due to garbage collection, these utilities provide defense-in-depth by overwriting buffer contents immediately and reducing the window where key material is accessible.
SecureKeyContainer
A secure container for holding private key material with controlled access and automatic cleanup.
Constructor
constructor(secretKey: Uint8Array)
Creates a new secure container holding a copy of the provided secret key.
The secret key to store securely. A copy is created internally.
After creating a SecureKeyContainer, you should immediately zero your original copy of the secret key using zeroMemory().
Properties
isCleared
Check if the key has been cleared from the container.
Returns: true if the key has been cleared, false otherwise.
Methods
useKey
async useKey<T>(callback: (secretKey: Uint8Array) => Promise<T>): Promise<T>
Execute an async callback with access to the secret key. The key is automatically cleared if an error occurs.
callback
(secretKey: Uint8Array) => Promise<T>
required
Async function that receives the secret key and returns a result. The key reference is only valid within this callback.
Returns: The result returned by the callback.
Throws: Error if the key has already been cleared.
Example:
const container = new SecureKeyContainer(secretKey)
try {
const result = await container.useKey(async (sk) => {
// Use the secret key for signing
const signed = transaction.signTxn(sk)
return signed
})
// Use result
} finally {
container.clear()
}
useKeySync
useKeySync<T>(callback: (secretKey: Uint8Array) => T): T
Execute a synchronous callback with access to the secret key.
callback
(secretKey: Uint8Array) => T
required
Synchronous function that receives the secret key and returns a result.
Returns: The result returned by the callback.
Throws: Error if the key has already been cleared.
Example:
const container = new SecureKeyContainer(secretKey)
try {
const result = container.useKeySync((sk) => {
// Perform synchronous operation
return deriveSomething(sk)
})
} finally {
container.clear()
}
clear
Securely clear the key from memory. This should be called when the key is no longer needed.
The key buffer is overwritten with zeros before the reference is released. This method is idempotent - calling it multiple times is safe.
Example:
const container = new SecureKeyContainer(secretKey)
// ... use the container
container.clear() // Key is zeroed and cleared
Functions
zeroMemory
function zeroMemory(buffer: Uint8Array): void
Securely zeros out a Uint8Array by overwriting all bytes with zeros. This helps prevent key material from lingering in memory.
The buffer to zero out. The buffer is modified in place.
The function uses a two-pass approach to prevent compiler optimizations from removing the zeroing operation:
- Fills the buffer with random values using
crypto.getRandomValues()
- Overwrites with zeros using
buffer.fill(0)
Example:
const privateKey = new Uint8Array(64)
// ... use the private key
zeroMemory(privateKey) // Securely erase from memory
zeroString
function zeroString(str: string): string
Securely zeros out a string by creating a mutable copy and clearing it. Returns an empty string.
The string to zero. Due to JavaScript string immutability, the original string may still exist in memory, but mutable references are cleared.
Returns: An empty string.
Due to JavaScript string immutability, this function cannot guarantee complete erasure of the original string from memory. Prefer using Uint8Array for sensitive data when possible.
Example:
let mnemonic = "word1 word2 word3 ..."
// ... use the mnemonic
mnemonic = zeroString(mnemonic) // Returns ""
withSecureKey
async function withSecureKey<T>(
secretKey: Uint8Array,
callback: (container: SecureKeyContainer) => Promise<T>
): Promise<T>
Execute an async function with a temporary secret key that is automatically cleared after the function completes (success or error). This is the preferred pattern for one-time key operations.
The secret key to protect. A SecureKeyContainer is created internally.
callback
(container: SecureKeyContainer) => Promise<T>
required
Async function that receives the secure container and returns a result.
Returns: The result returned by the callback.
The key is automatically cleared in a finally block, ensuring cleanup even if the callback throws an error.
Example:
import { withSecureKey } from '@txnlab/use-wallet'
const result = await withSecureKey(privateKey, async (container) => {
return await container.useKey(async (sk) => {
// Sign transaction with the secret key
return transaction.signTxn(sk)
})
})
// privateKey is automatically cleared at this point
withSecureKeySync
function withSecureKeySync<T>(
secretKey: Uint8Array,
callback: (container: SecureKeyContainer) => T
): T
Synchronous version of withSecureKey for non-async operations.
The secret key to protect. A SecureKeyContainer is created internally.
callback
(container: SecureKeyContainer) => T
required
Synchronous function that receives the secure container and returns a result.
Returns: The result returned by the callback.
Example:
import { withSecureKeySync } from '@txnlab/use-wallet'
const result = withSecureKeySync(privateKey, (container) => {
return container.useKeySync((sk) => {
// Perform synchronous cryptographic operation
return derivePublicKey(sk)
})
})
// privateKey is automatically cleared
Usage patterns
One-time operations
For one-time key operations, use withSecureKey or withSecureKeySync:
// Async operation
const signed = await withSecureKey(privateKey, async (container) => {
return await container.useKey(async (sk) => {
return transaction.signTxn(sk)
})
})
// Sync operation
const publicKey = withSecureKeySync(privateKey, (container) => {
return container.useKeySync((sk) => derivePublicKey(sk))
})
Multiple operations
For multiple operations with the same key, create a container and clear it when done:
const container = new SecureKeyContainer(privateKey)
try {
// Multiple operations
const result1 = await container.useKey(async (sk) => {
return await operation1(sk)
})
const result2 = await container.useKey(async (sk) => {
return await operation2(sk)
})
// Use results
} finally {
// Always clear in finally block
container.clear()
}
Wallet implementations
Wallets that handle raw private keys should use these utilities:
export class MyWallet extends BaseWallet {
private keyContainer: SecureKeyContainer | null = null
async connect() {
const secretKey = await getPrivateKeyFromSource()
this.keyContainer = new SecureKeyContainer(secretKey)
zeroMemory(secretKey) // Clear the original
// ... setup wallet
}
async signTransactions(txns: Transaction[]) {
if (!this.keyContainer) {
throw new Error('Wallet not connected')
}
return await this.keyContainer.useKey(async (sk) => {
return txns.map(txn => txn.signTxn(sk))
})
}
async disconnect() {
if (this.keyContainer) {
this.keyContainer.clear()
this.keyContainer = null
}
}
}
Security best practices
Never persist private keys to localStorage, cookies, or any storage mechanism. Keys should only exist in memory during active use and be cleared immediately after.
- Clear keys immediately - Always clear keys as soon as you’re done using them
- Use try/finally blocks - Ensure cleanup happens even if operations fail
- Prefer Uint8Array over strings - Binary data can be reliably zeroed, strings cannot
- Avoid logging keys - Never log, console.log, or display private keys
- Use scoped access - Keep key access confined to the smallest possible scope
- Never copy unnecessarily - Minimize the number of copies of key material in memory
Implementation notes
Memory zeroing technique
The zeroMemory function uses a two-pass approach to prevent compiler optimizations:
// Pass 1: Fill with random values to prevent optimization
crypto.getRandomValues(buffer)
// Pass 2: Zero out the buffer
buffer.fill(0)
This technique makes it more likely that the zeroing operation will not be optimized away by the JavaScript engine or JIT compiler.
String immutability
JavaScript strings are immutable, which means zeroString cannot guarantee complete erasure of string data from memory. For this reason:
- The secure key system uses
Uint8Array internally
- Mnemonics and other string-based keys should be converted to binary form as quickly as possible
- Never use strings for long-term key storage
Garbage collection
JavaScript’s garbage collector runs on its own schedule and may leave sensitive data in memory longer than desired. The secure key utilities mitigate this by:
- Overwriting key buffers immediately when cleared
- Setting references to
null to help the GC identify unused memory
- Reducing the window where key material is accessible
While not perfect, this approach significantly reduces the attack surface for memory-based attacks.