@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 +13 -0
- package/package.json +1 -1
- package/server/index.ts +2 -1
- package/server/lib/token-auth.ts +26 -0
- package/server/routes/openai-compat.ts +2 -1
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
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']
|
|
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
|
|
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
|
}
|