Skip to content

Commit f3d4f73

Browse files
vveerrggclaude
andcommitted
fix: remove hardcoded secrets and use cryptographic randomness
CRITICAL fixes: - Remove hardcoded private key from .env.test, add to .gitignore - Remove hardcoded JWT fallback secrets — fail if JWT_SECRET not set - Replace Math.random() challenge generation with crypto.randomBytes() (server) and crypto.getRandomValues() (browser) Note: the committed private key d217c1ff...ceecf should be considered compromised and rotated. Use git-filter-branch or BFG to remove from history if needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c41b093 commit f3d4f73

7 files changed

Lines changed: 28 additions & 11 deletions

File tree

.env.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ PORT=3002
22
NODE_ENV=development
33
TEST_MODE=true
44
LOG_LEVEL=debug
5-
SERVER_PRIVATE_KEY=d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf
5+
SERVER_PRIVATE_KEY=replace_with_test_key

.env.test.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
PORT=3002
2+
NODE_ENV=development
3+
TEST_MODE=true
4+
LOG_LEVEL=debug
5+
SERVER_PRIVATE_KEY=replace_with_test_key
6+
JWT_SECRET=replace_with_test_jwt_secret

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ dist/
1111
.env
1212
.env.local
1313
.env.*.local
14+
.env.test
1415
!.env.example
16+
!.env.test.example
1517

1618
# IDE
1719
.idea/

src/browser/nostr-browser-auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class NostrBrowserAuth {
4444
* @returns {Promise<{challenge: string, timestamp: number}>}
4545
*/
4646
async createChallenge(): Promise<{ challenge: string; timestamp: number }> {
47-
const challenge = `${Math.random().toString(36).substring(2, 15)}`;
47+
const challenge = Array.from(crypto.getRandomValues(new Uint8Array(32)), b => b.toString(16).padStart(2, '0')).join('');
4848
const timestamp = Math.floor(Date.now() / 1000);
4949
return { challenge, timestamp };
5050
}

src/config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ export const config: NostrAuthConfig = {
3939
challengePrefix: process.env.CHALLENGE_PREFIX || 'nostr:auth:',
4040
testMode: process.env.TEST_MODE === 'true',
4141
logLevel: process.env.LOG_LEVEL || 'info',
42-
jwtSecret: getEnvWithWarning('JWT_SECRET') || 'default-secret-do-not-use-in-production',
42+
jwtSecret: (() => {
43+
const secret = process.env.JWT_SECRET;
44+
if (!secret) throw new Error('FATAL: JWT_SECRET environment variable is required');
45+
return secret;
46+
})(),
4347
jwtExpiresIn: (process.env.JWT_EXPIRES_IN || '24h') as JWTExpiresIn,
4448
supabaseUrl: getEnvWithWarning('SUPABASE_URL'),
4549
supabaseKey: getEnvWithWarning('SUPABASE_KEY'),

src/config/index.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,11 @@ export const config: NostrConfig = {
5454
privateKey: process.env.SERVER_PRIVATE_KEY,
5555
keyManagementMode: (process.env.KEY_MANAGEMENT_MODE as 'development' | 'production') || 'development',
5656
// Auth config
57-
jwtSecret: process.env.JWT_SECRET || (process.env.NODE_ENV === 'production'
58-
? (() => { throw new Error('JWT_SECRET is required in production mode') })()
59-
: 'development_jwt_secret_do_not_use_in_production'),
57+
jwtSecret: (() => {
58+
const secret = process.env.JWT_SECRET;
59+
if (!secret) throw new Error('FATAL: JWT_SECRET environment variable is required');
60+
return secret;
61+
})(),
6062
jwtExpiresIn: '1h',
6163
testMode: process.env.TEST_MODE === 'true',
6264
// Optional configs
@@ -113,9 +115,11 @@ export async function loadConfig(envPath?: string): Promise<NostrConfig> {
113115
publicKey: process.env.SERVER_PUBLIC_KEY,
114116
keyManagementMode: process.env.KEY_MANAGEMENT_MODE as 'development' | 'production',
115117
// Auth config
116-
jwtSecret: process.env.JWT_SECRET || (process.env.NODE_ENV === 'production'
117-
? (() => { throw new Error('JWT_SECRET is required in production mode') })()
118-
: 'development_jwt_secret_do_not_use_in_production'),
118+
jwtSecret: (() => {
119+
const secret = process.env.JWT_SECRET;
120+
if (!secret) throw new Error('FATAL: JWT_SECRET environment variable is required');
121+
return secret;
122+
})(),
119123
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '24h',
120124
testMode: process.env.NODE_ENV !== 'production',
121125
// Optional configs

src/services/nostr.service.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @fileoverview Service for handling Nostr authentication operations
33
*/
44

5+
import crypto from 'crypto';
56
import { NostrEvent, NostrProfile, VerificationResult, NostrAuthConfig } from '../types.js';
67
import { createClient, SupabaseClient } from '@supabase/supabase-js';
78
import { validateEvent } from '../validators/event.validator.js';
@@ -51,8 +52,8 @@ export class NostrService {
5152
async createChallenge(pubkey: string): Promise<string> {
5253
const now = Math.floor(Date.now() / 1000);
5354
const challenge: SupabaseChallenge = {
54-
id: Math.random().toString(36).substring(7),
55-
challenge: `${this.config.challengePrefix || 'nostr-auth:'} ${Math.random().toString(36).substring(7)}`,
55+
id: crypto.randomBytes(32).toString('hex'),
56+
challenge: `${this.config.challengePrefix || 'nostr-auth:'} ${crypto.randomBytes(32).toString('hex')}`,
5657
created_at: now,
5758
expires_at: now + Math.floor(this.config.eventTimeoutMs / 1000),
5859
pubkey

0 commit comments

Comments
 (0)