termbeam 1.21.3 → 1.21.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.21.5] - 2026-04-23
4
+
5
+ - fix(auth): constant-time password comparison (#204) (@TickTockBent)
6
+ - fix(auth): use raw timingSafeEqual instead of HMAC for password compare (@dorlugasigal)
7
+ - fix(auth): pad to fixed length in safeCompare instead of self-compare (@dorlugasigal)
8
+
9
+ ## [1.21.4] - 2026-04-23
10
+
11
+ - feat: add author attribution to changelog via gh api (@dorlugasigal)
12
+ - chore(release): v1.21.3 [skip ci] (@github-actions[bot])
13
+ - fix: pass changelog section as release body (@dorlugasigal)
14
+
3
15
  ## [1.21.3] - 2026-04-23
4
16
 
5
17
  - feat: add author attribution to changelog via gh api (@dorlugasigal)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "1.21.3",
3
+ "version": "1.21.5",
4
4
  "description": "Beam your terminal to any device — mobile-optimized web terminal with multi-session support",
5
5
  "main": "src/server/index.js",
6
6
  "bin": {
@@ -290,6 +290,28 @@ const LOGIN_HTML = `<!DOCTYPE html>
290
290
  </body>
291
291
  </html>`;
292
292
 
293
+ // Constant-time string compare using crypto.timingSafeEqual on raw bytes.
294
+ // We intentionally do NOT hash the inputs: this is an equality check against
295
+ // an in-memory secret (never stored), and hashing would trip CodeQL's
296
+ // js/insufficient-password-hash rule which targets password *storage*.
297
+ //
298
+ // To avoid leaking length via early-return, both inputs are copied into
299
+ // fixed-length zero-padded buffers before timingSafeEqual, and the final
300
+ // result is AND-ed with a real length check.
301
+ const SAFE_COMPARE_LEN = 256;
302
+ function safeCompare(a, b) {
303
+ if (typeof a !== 'string' || typeof b !== 'string') return false;
304
+ const ab = Buffer.from(a, 'utf8');
305
+ const bb = Buffer.from(b, 'utf8');
306
+ if (ab.length > SAFE_COMPARE_LEN || bb.length > SAFE_COMPARE_LEN) return false;
307
+ const ap = Buffer.alloc(SAFE_COMPARE_LEN);
308
+ const bp = Buffer.alloc(SAFE_COMPARE_LEN);
309
+ ab.copy(ap);
310
+ bb.copy(bp);
311
+ const bytesEqual = crypto.timingSafeEqual(ap, bp);
312
+ return bytesEqual && ab.length === bb.length;
313
+ }
314
+
293
315
  function createAuth(password) {
294
316
  const tokens = new Map();
295
317
  const authAttempts = new Map();
@@ -373,7 +395,7 @@ function createAuth(password) {
373
395
  log.warn(`Auth: rate limit exceeded for ${ip}`);
374
396
  return res.status(429).json({ error: 'Too many attempts. Try again later.' });
375
397
  }
376
- if (authHeader === `Bearer ${password}`) return next();
398
+ if (safeCompare(authHeader.slice('Bearer '.length), password)) return next();
377
399
  recent.push(now);
378
400
  authAttempts.set(ip, recent);
379
401
  return res.status(401).json({ error: 'unauthorized' });
@@ -416,9 +438,10 @@ function createAuth(password) {
416
438
  middleware,
417
439
  rateLimit,
418
440
  parseCookies,
441
+ safeCompare,
419
442
  loginHTML: LOGIN_HTML,
420
443
  cleanup: () => clearInterval(cleanupInterval),
421
444
  };
422
445
  }
423
446
 
424
- module.exports = { createAuth };
447
+ module.exports = { createAuth, safeCompare };
@@ -133,7 +133,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager, copilotS
133
133
  // Auth API
134
134
  app.post('/api/auth', auth.rateLimit, (req, res) => {
135
135
  const { password } = req.body || {};
136
- if (password === auth.password) {
136
+ if (auth.safeCompare(password, auth.password)) {
137
137
  const token = auth.generateToken();
138
138
  res.cookie('pty_token', token, {
139
139
  httpOnly: true,
@@ -127,7 +127,7 @@ function setupWebSocket(wss, { auth, sessions, copilotService }) {
127
127
  return;
128
128
  }
129
129
 
130
- if (msg.password === auth.password || auth.validateToken(msg.token)) {
130
+ if (auth.safeCompare(msg.password, auth.password) || auth.validateToken(msg.token)) {
131
131
  authenticated = true;
132
132
  ws.send(JSON.stringify({ type: 'auth_ok' }));
133
133
  log.info('WS: auth success');