Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ console.log('Config Key:', configKey);
// e.g., "81aad4ab9dfd447cc479e6a4a7c9a544e2cafc7f3adeb68b2a21efad68eca4dc"
```

If you create the file with `generateSEBConfig`, generate the Config Key from `result.config`. This object contains the schema defaults and the `looseConfig` values that are written to the file:

```typescript
const result = await generateSEBConfig(config, {}, looseConfig);
const configKey = generateConfigKey(result.config);
```

### Server-side: Verify Config Key from HTTP Headers

When SEB sends requests, it includes the Config Key hash in the `X-SafeExamBrowser-ConfigKeyHash` header:
Expand Down
24 changes: 24 additions & 0 deletions src/__tests__/config-key.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,30 @@ describe('config-key', () => {
});
});

describe('floating point values', () => {
// Expected values from C printf "%.15g" with uppercase exponent marker
it.each([
[0.1, '0.1'],
[-2.25, '-2.25'],
[0.1 + 0.2, '0.3'],
[1 / 3, '0.333333333333333'],
[123.456, '123.456'],
[0.0001, '0.0001'],
[0.00001, '1E-05'],
[1.5e-7, '1.5E-07'],
[-0.00012345, '-0.00012345'],
[123456789012345.6, '123456789012346'],
[1234567890123456.8, '1.23456789012346E+15'],
[99999999999999.98, '100000000000000'],
])('should format %d like SEB', (value, expected) => {
expect(convertToSEBJSON({ value })).toBe(`{"value":${expected}}`);
});

it('should keep integers and negative zero as integers', () => {
expect(convertToSEBJSON({ a: 1, b: -0 })).toBe('{"a":1,"b":0}');
});
});

describe('generateConfigKeyHash', () => {
it('should generate 64-character hex string', () => {
const configKey = 'a'.repeat(64);
Expand Down
23 changes: 23 additions & 0 deletions src/__tests__/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,29 @@ describe('config generator', () => {
expect(result.xml).toContain('unknownKey');
expect(result.xml).toContain('some value');
});

it('should return the config written to the file for Config Key generation', async () => {
const config = {
startURL: 'https://exam.example.com',
};

const result = await generateSEBConfig(
config,
{},
{
unknownKey: 'some value',
},
);
const fileConfig = parsePlist(await decompressSEBFile(result.data));

expect(result.config.unknownKey).toBe('some value');
expect(generateConfigKey(result.config)).toBe(
generateConfigKey(fileConfig),
);
expect(generateConfigKey(result.config)).not.toBe(
generateConfigKey(config),
);
});
});

describe('generatePlainSEB', () => {
Expand Down
23 changes: 23 additions & 0 deletions src/__tests__/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ describe('schema', () => {
expect(result.success).toBe(true);
});

it('should validate SEB for macOS 3.7 settings', () => {
const config = {
startURL: 'https://exam.example.com',
lockdownModePolicy: 1,
detectAccessibilityApps: true,
hideWiFiControls: false,
sebAllowedVersions: ['Mac.3.7.min'],
screenProctoringAACCapturePolicy: 2,
permittedProcesses: [
{
executable: 'App',
allowAccessibility: true,
teamIdentifier: 'ABCDE12345',
},
],
};

expect(sebConfigSchema.safeParse(config).success).toBe(true);
expect(
sebConfigSchema.safeParse({ ...config, lockdownModePolicy: 3 }).success,
).toBe(false);
});

it('should reject invalid URL', () => {
const config = {
startURL: 'not-a-valid-url',
Expand Down
32 changes: 31 additions & 1 deletion src/config-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function stringifySEBJSON(value: unknown): string {
}

if (typeof value === 'number') {
return value.toString();
return Number.isInteger(value) ? value.toString() : formatSEBReal(value);
}

if (typeof value === 'string') {
Expand Down Expand Up @@ -95,6 +95,36 @@ function stringifySEBJSON(value: unknown): string {
return 'null';
}

/**
* Formats a floating point value like SEB does for the Config Key: C "%.15g" with an
* uppercase exponent marker, which matches "G15" of SEB for Windows (SEB for macOS 3.7+).
* Unverified: values exactly halfway between two 15 digit results can round differently.
* @see https://github.com/SafeExamBrowser/seb-win-refactoring/issues/1495
*/
function formatSEBReal(value: number): string {
if (!Number.isFinite(value)) {
return value.toString();
}

const [mantissa, exponentString] = value.toExponential(14).split('e');
const exponent = Number(exponentString);

if (exponent < -4 || exponent >= 15) {
const trimmedMantissa = mantissa.replace(/0+$/, '').replace(/\.$/, '');
const exponentSign = exponent < 0 ? '-' : '+';
const exponentDigits = String(Math.abs(exponent)).padStart(2, '0');
return `${trimmedMantissa}E${exponentSign}${exponentDigits}`;
}

const sign = value < 0 ? '-' : '';
const digits = mantissa.replace('-', '').replace('.', '');
const fixed =
exponent >= 0
? `${digits.slice(0, exponent + 1)}.${digits.slice(exponent + 1)}`
: `0.${'0'.repeat(-exponent - 1)}${digits}`;
return sign + fixed.replace(/0+$/, '').replace(/\.$/, '');
}

/**
* Generates the Config Key hash from a SEB configuration
*
Expand Down
8 changes: 8 additions & 0 deletions src/generator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SEBConfigObject } from './config-key.js';
import type { SEBConfig } from './schema.js';
import crypto from 'node:crypto';
import { promisify } from 'node:util';
Expand Down Expand Up @@ -42,6 +43,12 @@ export interface SEBGenerateResult {
* File size in bytes
*/
size: number;

/**
* The config object written to the file, after validation defaults and looseConfig.
* Use this object with generateConfigKey, so the Config Key matches the file.
*/
config: SEBConfigObject;
}

/**
Expand Down Expand Up @@ -103,6 +110,7 @@ export async function generateSEBConfig(
data: compressedData,
xml: plistXml,
size: compressedData.length,
config: validatedConfig as unknown as SEBConfigObject,
};
}

Expand Down
Loading
Loading