Skip to content

Commit 3d8f71d

Browse files
committed
Fix login primitive smoke fallback
1 parent 472ddb9 commit 3d8f71d

4 files changed

Lines changed: 96 additions & 3 deletions

File tree

.github/workflows/cli-release-build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ jobs:
184184
# Fast path: --version exits synchronously through commander, so it
185185
# only catches early sync failures. Run it for parity with old CI.
186186
"$BIN" --version
187+
"$BIN" --smoke-login-primitives
187188
188189
# Slow path: keep the binary alive long enough for *async* startup
189190
# failures (e.g. the Parser.init rejection that crashed the

.github/workflows/freebuff-e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ jobs:
4444
# startup failures (e.g. the Parser.init rejection from a broken
4545
# tree-sitter wasm load).
4646
cli/bin/freebuff --version
47+
cli/bin/freebuff --smoke-login-primitives
4748
# Run for a few seconds so unhandled rejections during module init
4849
# have a chance to fire and trip earlyFatalHandler.
4950
bun cli/scripts/smoke-binary.ts cli/bin/freebuff

cli/src/utils/__tests__/fingerprint.test.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { describe, test, expect } from 'bun:test'
1+
import { describe, test, expect, spyOn } from 'bun:test'
22

3-
import { getFingerprintType, generateFingerprintIdSync } from '../fingerprint'
3+
import {
4+
getFingerprintType,
5+
generateFingerprintIdSync,
6+
generateLegacyFingerprintSuffix,
7+
} from '../fingerprint'
48

59
describe('fingerprint utilities', () => {
610
describe('getFingerprintType', () => {
@@ -142,4 +146,42 @@ describe('fingerprint utilities', () => {
142146
})
143147
})
144148

149+
describe('generateLegacyFingerprintSuffix', () => {
150+
test('falls back to Web Crypto when node randomBytes fails', () => {
151+
const suffix = generateLegacyFingerprintSuffix(
152+
() => {
153+
throw new Error('randomBytes unavailable')
154+
},
155+
{
156+
getRandomValues: (bytes) => {
157+
bytes.set([0, 1, 2, 3, 4, 5])
158+
return bytes
159+
},
160+
},
161+
)
162+
163+
expect(suffix).toBe('AAECAwQF')
164+
})
165+
166+
test('falls back to Math.random when node and Web Crypto both fail', () => {
167+
const mathRandomSpy = spyOn(Math, 'random').mockReturnValue(0)
168+
169+
try {
170+
const suffix = generateLegacyFingerprintSuffix(
171+
() => {
172+
throw new Error('randomBytes unavailable')
173+
},
174+
{
175+
getRandomValues: () => {
176+
throw new Error('getRandomValues unavailable')
177+
},
178+
},
179+
)
180+
181+
expect(suffix).toBe('AAAAAAAA')
182+
} finally {
183+
mathRandomSpy.mockRestore()
184+
}
185+
})
186+
})
145187
})

cli/src/utils/fingerprint.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ let machineIdModule: typeof import('node-machine-id') | null = null
2222
let systeminformationModule: typeof import('systeminformation') | null = null
2323

2424
const ENHANCED_FINGERPRINT_TIMEOUT_MS = 3000
25+
const LEGACY_FINGERPRINT_SUFFIX_LENGTH = 8
26+
const LEGACY_FINGERPRINT_RANDOM_BYTES = 6
27+
const BASE64URL_ALPHABET =
28+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
29+
30+
type RandomValuesProvider = {
31+
getRandomValues: (bytes: Uint8Array) => Uint8Array
32+
}
2533

2634
async function getMachineId(): Promise<string> {
2735
if (!machineIdModule) {
@@ -136,10 +144,51 @@ async function calculateEnhancedFingerprint(): Promise<string> {
136144
* Used as a fallback when enhanced fingerprinting fails.
137145
*/
138146
function calculateLegacyFingerprint(): string {
139-
const randomSuffix = randomBytes(6).toString('base64url').substring(0, 8)
147+
const randomSuffix = generateLegacyFingerprintSuffix()
140148
return `codebuff-cli-${randomSuffix}`
141149
}
142150

151+
export function generateLegacyFingerprintSuffix(
152+
randomByteSource: (byteCount: number) => Uint8Array = randomBytes,
153+
randomValuesProvider: RandomValuesProvider | undefined = globalThis.crypto,
154+
): string {
155+
try {
156+
return Buffer.from(randomByteSource(LEGACY_FINGERPRINT_RANDOM_BYTES))
157+
.toString('base64url')
158+
.substring(0, LEGACY_FINGERPRINT_SUFFIX_LENGTH)
159+
} catch (err) {
160+
logger.warn(
161+
{
162+
errorMessage: err instanceof Error ? err.message : String(err),
163+
},
164+
'Node crypto randomBytes failed for legacy fingerprint suffix',
165+
)
166+
}
167+
168+
try {
169+
if (randomValuesProvider) {
170+
const bytes = new Uint8Array(LEGACY_FINGERPRINT_RANDOM_BYTES)
171+
randomValuesProvider.getRandomValues(bytes)
172+
return Buffer.from(bytes)
173+
.toString('base64url')
174+
.substring(0, LEGACY_FINGERPRINT_SUFFIX_LENGTH)
175+
}
176+
} catch (err) {
177+
logger.warn(
178+
{
179+
errorMessage: err instanceof Error ? err.message : String(err),
180+
},
181+
'Web Crypto getRandomValues failed for legacy fingerprint suffix',
182+
)
183+
}
184+
185+
let suffix = ''
186+
for (let i = 0; i < LEGACY_FINGERPRINT_SUFFIX_LENGTH; i++) {
187+
suffix += BASE64URL_ALPHABET[Math.floor(Math.random() * BASE64URL_ALPHABET.length)]
188+
}
189+
return suffix
190+
}
191+
143192
/**
144193
* Cached fingerprint promise. Populated on first call and reused for the
145194
* process lifetime so every auth step in a session ships the same fingerprint

0 commit comments

Comments
 (0)