Skip to content

Commit 2be7a83

Browse files
vveerrggclaude
andcommitted
fix: memory zeroing for shared secrets and npub input rejection
Zero shared secret buffers after AES key import in encrypt/decrypt. Zero private key bytes after public key derivation in generateKeyPair. Reject npub-prefixed inputs in getPublicKeyHex with guidance to use nip19.decode(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d4574cd commit 2be7a83

3 files changed

Lines changed: 21 additions & 1 deletion

File tree

src/crypto.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export function getSchnorrPublicKey(privateKeyBytes: Uint8Array): Uint8Array {
151151
export async function generateKeyPair(): Promise<KeyPair> {
152152
const privateKeyBytes = randomBytes(32);
153153
const privateKey = bytesToHex(privateKeyBytes);
154+
privateKeyBytes.fill(0); // zero source material
154155
const publicKey = await getPublicKey(privateKey);
155156

156157
return {
@@ -298,6 +299,10 @@ export async function encrypt(
298299
['encrypt']
299300
));
300301

302+
// Zero shared secret material now that AES key is imported
303+
sharedX.fill(0);
304+
sharedPoint.fill(0);
305+
301306
// Encrypt the message
302307
const data = new TextEncoder().encode(message);
303308
const encrypted = await customCrypto.getSubtle().then((subtle) => subtle.encrypt(
@@ -355,6 +360,10 @@ export async function decrypt(
355360
['decrypt']
356361
));
357362

363+
// Zero shared secret material now that AES key is imported
364+
sharedX.fill(0);
365+
sharedPoint.fill(0);
366+
358367
const decrypted = await customCrypto.getSubtle().then((subtle) => subtle.decrypt(
359368
{ name: 'AES-CBC', iv },
360369
key,

src/nips/nip-04.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ export async function encryptMessage(
117117
['encrypt']
118118
);
119119

120+
// Zero shared secret material now that AES key is imported
121+
sharedX.fill(0);
122+
sharedPoint.fill(0);
123+
120124
// Generate IV and encrypt
121125
const iv = new Uint8Array(16);
122126
await cryptoImpl.getRandomValues(iv);
@@ -179,6 +183,10 @@ export async function decryptMessage(
179183
['decrypt']
180184
);
181185

186+
// Zero shared secret material now that AES key is imported
187+
sharedX.fill(0);
188+
sharedPoint.fill(0);
189+
182190
// Parse NIP-04 standard format: base64(ciphertext) + "?iv=" + base64(iv)
183191
// Also support legacy hex format (iv + ciphertext concatenated) as fallback
184192
let iv: Uint8Array;

src/utils/validation.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ export function validateEvent(event: NostrEvent): ValidationResult {
7474
* @returns Hex representation of the public key
7575
*/
7676
export function getPublicKeyHex(pubkey: string): string {
77-
return pubkey.startsWith('npub1') ? pubkey.slice(5) : pubkey;
77+
if (pubkey.startsWith('npub1')) {
78+
throw new Error('npub inputs require bech32 decoding — use nip19.decode() instead');
79+
}
80+
return pubkey;
7881
}
7982

8083
/**

0 commit comments

Comments
 (0)