Skip to content

Commit 033eae7

Browse files
vveerrggclaude
andcommitted
fix: memory zeroing, Schnorr signatures, and 32-byte key derivation
HIGH fixes: - Zero Uint8Array buffers containing entropy/private key bytes after use (.fill(0) on all intermediate key material in 4 files) - Replace ECDSA secp256k1.sign/verify with schnorr.sign/verify (NIP-01 requires BIP-340 Schnorr signatures, not ECDSA) - Hash 16-byte mnemonic entropy to 32 bytes via SHA-256 in core/keys.ts (was returning raw entropy as private key — invalid for secp256k1) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a3f7605 commit 033eae7

4 files changed

Lines changed: 38 additions & 16 deletions

File tree

src/core/crypto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export async function getSharedSecret(
9797
const privKeyBytes = hexToBytes(privateKey);
9898
const pubKeyBytes = hexToBytes(publicKey);
9999
const sharedPoint = schnorr.getPublicKey(privKeyBytes);
100+
privKeyBytes.fill(0); // zero sensitive material
100101
return sha256(sharedPoint);
101102
} catch (error) {
102103
logger.error("Failed to get shared secret:", error?.toString());

src/core/keys.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as secp256k1 from "@noble/secp256k1";
22
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
3+
import { sha256 } from "@noble/hashes/sha256";
34
import { pino } from "pino";
45
import { KeyPair } from "../types/keys.js";
56
import {
@@ -155,7 +156,9 @@ export function seedPhraseToPrivateKey(seedPhrase: string): string {
155156
}
156157

157158
const entropy = getEntropyFromSeedPhrase(seedPhrase);
158-
return bytesToHex(entropy);
159+
const privateKey = bytesToHex(sha256(entropy));
160+
entropy.fill(0); // zero sensitive material
161+
return privateKey;
159162
} catch (error) {
160163
logger.error("Failed to convert seed phrase to private key:", error);
161164
throw new Error("Failed to convert seed phrase to private key");

src/crypto/keys.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,12 @@ export async function seedPhraseToKeyPair(
101101

102102
const entropy = getEntropyFromSeedPhrase(seedPhrase);
103103
const privateKey = derivePrivateKey(entropy);
104+
entropy.fill(0); // zero sensitive material
105+
const privateKeyBytes = hexToBytes(privateKey);
104106
const publicKey = createPublicKey(
105-
bytesToHex(getCompressedPublicKey(hexToBytes(privateKey))),
107+
bytesToHex(getCompressedPublicKey(privateKeyBytes)),
106108
);
109+
privateKeyBytes.fill(0); // zero sensitive material
107110

108111
return {
109112
privateKey,
@@ -127,10 +130,11 @@ export async function seedPhraseToKeyPair(
127130
*/
128131
export function derivePrivateKey(entropy: Uint8Array): string {
129132
try {
130-
let privateKeyBytes = entropy;
131133
// Hash the entropy to get a valid private key
132-
privateKeyBytes = sha256(privateKeyBytes);
133-
return bytesToHex(privateKeyBytes);
134+
const privateKeyBytes = sha256(entropy);
135+
const hex = bytesToHex(privateKeyBytes);
136+
privateKeyBytes.fill(0); // zero sensitive material
137+
return hex;
134138
} catch (error) {
135139
logger.error("Failed to derive private key:", error?.toString());
136140
throw new Error("Failed to derive private key");
@@ -156,11 +160,13 @@ export async function fromHex(privateKeyHex: string): Promise<KeyPair> {
156160
try {
157161
const privateKeyBytes = hexToBytes(privateKeyHex);
158162
if (!secp256k1.utils.isValidPrivateKey(privateKeyBytes)) {
163+
privateKeyBytes.fill(0); // zero sensitive material
159164
throw new Error("Invalid private key");
160165
}
161166
const publicKey = createPublicKey(
162167
bytesToHex(getCompressedPublicKey(privateKeyBytes)),
163168
);
169+
privateKeyBytes.fill(0); // zero sensitive material
164170

165171
return {
166172
privateKey: privateKeyHex,
@@ -187,6 +193,7 @@ export async function validateKeyPair(
187193
try {
188194
const privateKeyBytes = hexToBytes(privateKey);
189195
if (!secp256k1.utils.isValidPrivateKey(privateKeyBytes)) {
196+
privateKeyBytes.fill(0); // zero sensitive material
190197
return {
191198
isValid: false,
192199
error: "Invalid private key",
@@ -197,6 +204,7 @@ export async function validateKeyPair(
197204
const derivedPublicKey = bytesToHex(
198205
getCompressedPublicKey(privateKeyBytes),
199206
);
207+
privateKeyBytes.fill(0); // zero sensitive material
200208

201209
if (pubKeyHex !== derivedPublicKey) {
202210
return {

src/index.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { generateMnemonic, validateMnemonic, mnemonicToEntropy } from "bip39";
22
import * as secp256k1 from "@noble/secp256k1";
3+
import { schnorr } from "@noble/curves/secp256k1";
34
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
45
import { sha256 } from "@noble/hashes/sha256";
56
import { hmac } from "@noble/hashes/hmac";
@@ -124,7 +125,9 @@ export function seedPhraseToKeyPair(seedPhrase: string): KeyPair {
124125
const entropy = getEntropyFromSeedPhrase(seedPhrase);
125126
// Hash the entropy to generate a proper private key
126127
const privateKeyBytes = sha256(entropy);
128+
entropy.fill(0); // zero sensitive material
127129
const privateKeyHex = bytesToHex(privateKeyBytes);
130+
privateKeyBytes.fill(0); // zero sensitive material
128131

129132
// Derive the public key
130133
const publicKeyBytes = secp256k1.getPublicKey(privateKeyHex, true); // Force compressed format
@@ -180,11 +183,13 @@ export function fromHex(privateKeyHex: string): KeyPair {
180183
// Validate the private key
181184
const privateKeyBytes = hexToBytes(privateKeyHex);
182185
if (!secp256k1.utils.isValidPrivateKey(privateKeyBytes)) {
186+
privateKeyBytes.fill(0); // zero sensitive material
183187
throw new Error("Invalid private key");
184188
}
185189

186190
// Derive the public key
187191
const publicKeyBytes = secp256k1.getPublicKey(privateKeyBytes, true); // Force compressed format
192+
privateKeyBytes.fill(0); // zero sensitive material
188193
const publicKey = bytesToHex(publicKeyBytes);
189194

190195
// Generate the nsec and npub formats
@@ -216,6 +221,7 @@ export function getPublicKey(privateKey: string): string {
216221
try {
217222
const privateKeyBytes = hexToBytes(privateKey);
218223
const publicKeyBytes = secp256k1.getPublicKey(privateKeyBytes, true); // Force compressed format
224+
privateKeyBytes.fill(0); // zero sensitive material
219225
return bytesToHex(publicKeyBytes);
220226
} catch (error) {
221227
logger.error("Failed to get public key:", error?.toString());
@@ -358,12 +364,11 @@ export async function signEvent(
358364
): Promise<string> {
359365
try {
360366
const eventHash = getEventHash(event);
361-
const signature = await secp256k1.sign(
362-
hexToBytes(eventHash),
363-
hexToBytes(privateKey),
364-
);
367+
const privateKeyBytes = hexToBytes(privateKey);
368+
const signature = schnorr.sign(eventHash, privateKeyBytes);
369+
privateKeyBytes.fill(0); // zero sensitive material
365370
logger.log("Event signed successfully");
366-
return bytesToHex(signature.toCompactRawBytes());
371+
return bytesToHex(signature);
367372
} catch (error) {
368373
logger.error("Failed to sign event:", error?.toString());
369374
throw error;
@@ -392,9 +397,9 @@ export async function verifyEvent(event: NostrEvent): Promise<boolean> {
392397
}
393398

394399
logger.log("Verifying event signature");
395-
return await secp256k1.verify(
400+
return schnorr.verify(
396401
hexToBytes(event.sig),
397-
hexToBytes(hash),
402+
hash,
398403
hexToBytes(event.pubkey),
399404
);
400405
} catch (error) {
@@ -527,6 +532,7 @@ export function privateKeyToNpub(privateKey: string): string {
527532
try {
528533
const privateKeyBytes = hexToBytes(privateKey);
529534
const publicKey = secp256k1.getPublicKey(privateKeyBytes, true);
535+
privateKeyBytes.fill(0); // zero sensitive material
530536
return nip19.npubEncode(bytesToHex(publicKey));
531537
} catch (error) {
532538
logger.error("Failed to encode npub:", error?.toString());
@@ -632,9 +638,12 @@ export async function signMessage(
632638
try {
633639
const messageBytes = new TextEncoder().encode(message);
634640
const messageHash = sha256(messageBytes);
635-
const signature = await secp256k1.sign(messageHash, hexToBytes(privateKey));
641+
const messageHashHex = bytesToHex(messageHash);
642+
const privateKeyBytes = hexToBytes(privateKey);
643+
const signature = schnorr.sign(messageHashHex, privateKeyBytes);
644+
privateKeyBytes.fill(0); // zero sensitive material
636645
logger.log("Message signed successfully");
637-
return bytesToHex(signature.toCompactRawBytes());
646+
return bytesToHex(signature);
638647
} catch (error) {
639648
logger.error("Failed to sign message:", error?.toString());
640649
throw error;
@@ -659,10 +668,11 @@ export async function verifySignature(
659668
try {
660669
const messageBytes = new TextEncoder().encode(message);
661670
const messageHash = sha256(messageBytes);
671+
const messageHashHex = bytesToHex(messageHash);
662672
logger.log("Verifying message signature");
663-
return await secp256k1.verify(
673+
return schnorr.verify(
664674
hexToBytes(signature),
665-
messageHash,
675+
messageHashHex,
666676
hexToBytes(publicKey),
667677
);
668678
} catch (error) {

0 commit comments

Comments
 (0)