@gotcos/glasses-server 6.12.3 → 6.12.4

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 6.12.4
4
+
5
+ Completes the public launch security review with a constant-time token check.
6
+
7
+ - **API tokens compare in constant time.** The `/api` middleware and the
8
+ OpenAI-compatible `/v1/chat/completions` Bearer check no longer use a plain
9
+ `!==` string compare, which short-circuits on the first differing byte and
10
+ leaks token bytes through response timing. Both now hash each side to a fixed
11
+ SHA-256 digest and compare with `crypto.timingSafeEqual`. Missing headers,
12
+ duplicated headers, and length-mismatched tokens fail closed without throwing.
13
+ A new `token-auth` test pins the behavior. 401 responses are otherwise
14
+ unchanged.
15
+
3
16
  ## 6.12.3
4
17
 
5
18
  Security hardening from the public launch review, without changing app/server
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.12.3",
3
+ "version": "6.12.4",
4
4
  "description": "COS Glasses — self-hosted AI heads-up-display server for Even G2 smart glasses, powered by your local Claude Code or Codex CLI",
5
5
  "type": "module",
6
6
  "bin": {
package/server/index.ts CHANGED
@@ -57,6 +57,7 @@ import {
57
57
  isAllowedNetworkOrigin,
58
58
  isTailscaleIpv4,
59
59
  } from './lib/network-policy.js'
60
+ import { timingSafeTokenEqual } from './lib/token-auth.js'
60
61
 
61
62
  const app = express()
62
63
  const PORT = parseInt(process.env.PORT ?? '3141', 10)
@@ -128,7 +129,7 @@ app.use('/api', (req, res, next) => {
128
129
  req.path === '/diag/client' ||
129
130
  req.path === '/diag/health'
130
131
  ) return next()
131
- if (req.headers['x-cos-token'] !== API_TOKEN) {
132
+ if (!timingSafeTokenEqual(req.headers['x-cos-token'], API_TOKEN)) {
132
133
  return res.status(401).json({ error: 'unauthorized' })
133
134
  }
134
135
  next()
@@ -0,0 +1,26 @@
1
+ // Constant-time API-token comparison for the public server.
2
+
3
+ import { createHash, timingSafeEqual } from 'node:crypto'
4
+
5
+ /**
6
+ * Compare a client-supplied API token against the expected token without
7
+ * leaking token bytes through response timing.
8
+ *
9
+ * A plain `provided !== expected` string compare short-circuits at the first
10
+ * differing byte, so an attacker measuring response latency can recover the
11
+ * token one byte at a time. Hashing both sides to a fixed 32-byte SHA-256
12
+ * digest before `timingSafeEqual` keeps the comparison constant time
13
+ * regardless of input length, and avoids `timingSafeEqual` throwing on
14
+ * length-mismatched buffers.
15
+ *
16
+ * Non-string input (missing header, duplicated header parsed as an array) and
17
+ * an empty expected token never match.
18
+ */
19
+ export function timingSafeTokenEqual(provided: unknown, expected: string): boolean {
20
+ if (typeof provided !== 'string' || typeof expected !== 'string' || expected.length === 0) {
21
+ return false
22
+ }
23
+ const a = createHash('sha256').update(provided).digest()
24
+ const b = createHash('sha256').update(expected).digest()
25
+ return timingSafeEqual(a, b)
26
+ }
@@ -13,6 +13,7 @@ import {
13
13
  } from '../lib/codex-model-catalog.js'
14
14
  import { tryInstantResponse } from '../lib/response-cache.js'
15
15
  import crypto from 'node:crypto'
16
+ import { timingSafeTokenEqual } from '../lib/token-auth.js'
16
17
 
17
18
  export const openaiCompatRouter = Router()
18
19
 
@@ -66,7 +67,7 @@ function validateAuth(req: any, res: any): boolean {
66
67
  }
67
68
 
68
69
  const token = auth.slice(7)
69
- if (token !== cosToken) {
70
+ if (!timingSafeTokenEqual(token, cosToken)) {
70
71
  res.status(401).json({ error: { message: 'Invalid token', type: 'invalid_request_error' } })
71
72
  return false
72
73
  }