Skip to content

Commit 9a64d7d

Browse files
authored
fix: salting the hash, subject attributes type (#178)
* fix salting and hashing * tests for salt * Context attributes only in the Configuration Wire
1 parent ccffe56 commit 9a64d7d

File tree

5 files changed

+49
-46
lines changed

5 files changed

+49
-46
lines changed

src/client/eppo-client.spec.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -246,11 +246,7 @@ describe('EppoClient E2E test', () => {
246246

247247
it('obfuscates assignments', () => {
248248
// Use a known salt to produce deterministic hashes
249-
setSaltOverrideForTests({
250-
base64String: 'BzURTg==',
251-
saltString: '0735114e',
252-
bytes: new Uint8Array([7, 53, 17, 78]),
253-
});
249+
setSaltOverrideForTests(new Uint8Array([7, 53, 17, 78]));
254250

255251
const encodedPrecomputedWire = client.getPrecomputedAssignments('subject', {}, true);
256252
const { precomputed } = JSON.parse(encodedPrecomputedWire) as IConfigurationWire;
@@ -263,23 +259,23 @@ describe('EppoClient E2E test', () => {
263259
expect(precomputedResponse.salt).toEqual('BzURTg==');
264260

265261
const precomputedFlags = precomputedResponse?.flags ?? {};
266-
expect(Object.keys(precomputedFlags)).toContain('ddc24ede545855b9bbae82cfec6a83a1'); // flagKey, md5 hashed
267-
expect(Object.keys(precomputedFlags)).toContain('2b439e5a0104d62400dc44c34230f6f2'); // 'anotherFlag', md5 hashed
262+
expect(Object.keys(precomputedFlags)).toContain('34863af2a6019c80e054c6997241b3d5'); // flagKey, md5 hashed
263+
expect(Object.keys(precomputedFlags)).toContain('076aaf100f76cb2f93205dc6cd05f756'); // 'anotherFlag', md5 hashed
268264

269265
const decodedFirstFlag = decodePrecomputedFlag(
270-
precomputedFlags['ddc24ede545855b9bbae82cfec6a83a1'],
266+
precomputedFlags['34863af2a6019c80e054c6997241b3d5'],
271267
);
272-
expect(decodedFirstFlag.flagKey).toEqual('ddc24ede545855b9bbae82cfec6a83a1');
268+
expect(decodedFirstFlag.flagKey).toEqual('34863af2a6019c80e054c6997241b3d5');
273269
expect(decodedFirstFlag.variationType).toEqual(VariationType.STRING);
274270
expect(decodedFirstFlag.variationKey).toEqual('a');
275271
expect(decodedFirstFlag.variationValue).toEqual('variation-a');
276272
expect(decodedFirstFlag.doLog).toEqual(true);
277273
expect(decodedFirstFlag.extraLogging).toEqual({});
278274

279275
const decodedSecondFlag = decodePrecomputedFlag(
280-
precomputedFlags['2b439e5a0104d62400dc44c34230f6f2'],
276+
precomputedFlags['076aaf100f76cb2f93205dc6cd05f756'],
281277
);
282-
expect(decodedSecondFlag.flagKey).toEqual('2b439e5a0104d62400dc44c34230f6f2');
278+
expect(decodedSecondFlag.flagKey).toEqual('076aaf100f76cb2f93205dc6cd05f756');
283279
expect(decodedSecondFlag.variationType).toEqual(VariationType.STRING);
284280
expect(decodedSecondFlag.variationKey).toEqual('b');
285281
expect(decodedSecondFlag.variationValue).toEqual('variation-b');

src/client/eppo-client.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -939,23 +939,26 @@ export default class EppoClient {
939939
*/
940940
getPrecomputedAssignments(
941941
subjectKey: string,
942-
subjectAttributes: Attributes = {},
942+
subjectAttributes: Attributes | ContextAttributes = {},
943943
obfuscated = false,
944944
): string {
945945
const configDetails = this.getConfigDetails();
946-
const flags = this.getAllAssignments(subjectKey, subjectAttributes);
946+
947+
const subjectContextualAttributes = this.ensureContextualSubjectAttributes(subjectAttributes);
948+
const subjectFlatAttributes = this.ensureNonContextualSubjectAttributes(subjectAttributes);
949+
const flags = this.getAllAssignments(subjectKey, subjectFlatAttributes);
947950

948951
const precomputedConfig: IPrecomputedConfiguration = obfuscated
949952
? new ObfuscatedPrecomputedConfiguration(
950953
subjectKey,
951954
flags,
952-
subjectAttributes,
955+
subjectContextualAttributes,
953956
configDetails.configEnvironment,
954957
)
955958
: new PrecomputedConfiguration(
956959
subjectKey,
957960
flags,
958-
subjectAttributes,
961+
subjectContextualAttributes,
959962
configDetails.configEnvironment,
960963
);
961964

src/configuration.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Environment, FormatEnum, PrecomputedFlag } from './interfaces';
2-
import { generateSalt, obfuscatePrecomputedFlags, Salt } from './obfuscation';
3-
import { Attributes, ContextAttributes } from './types';
2+
import { generateSalt, obfuscatePrecomputedFlags, ISalt } from './obfuscation';
3+
import { ContextAttributes } from './types';
44

55
export interface IPrecomputedConfigurationResponse {
66
// `format` is always `PRECOMPUTED`
@@ -19,7 +19,7 @@ export interface IPrecomputedConfiguration {
1919
readonly response: string;
2020
readonly subjectKey: string;
2121
// Optional in case server does not want to expose attributes to a client.
22-
readonly subjectAttributes?: Attributes | ContextAttributes;
22+
readonly subjectAttributes?: ContextAttributes;
2323
}
2424

2525
export class PrecomputedConfiguration implements IPrecomputedConfiguration {
@@ -29,7 +29,7 @@ export class PrecomputedConfiguration implements IPrecomputedConfiguration {
2929
constructor(
3030
readonly subjectKey: string,
3131
flags: Record<string, PrecomputedFlag>,
32-
readonly subjectAttributes?: Attributes | ContextAttributes,
32+
readonly subjectAttributes?: ContextAttributes,
3333
environment?: Environment,
3434
) {
3535
const precomputedResponse: IPrecomputedConfigurationResponse = {
@@ -47,12 +47,12 @@ export class PrecomputedConfiguration implements IPrecomputedConfiguration {
4747
export class ObfuscatedPrecomputedConfiguration implements IPrecomputedConfiguration {
4848
readonly format = FormatEnum.PRECOMPUTED;
4949
readonly response: string;
50-
private saltBase: Salt;
50+
private saltBase: ISalt;
5151

5252
constructor(
5353
readonly subjectKey: string,
5454
flags: Record<string, PrecomputedFlag>,
55-
readonly subjectAttributes?: Attributes | ContextAttributes,
55+
readonly subjectAttributes?: ContextAttributes,
5656
environment?: Environment,
5757
) {
5858
this.saltBase = generateSalt();

src/obfuscation.spec.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { decodeBase64, encodeBase64 } from './obfuscation';
1+
import { decodeBase64, encodeBase64, Salt } from './obfuscation';
22

33
describe('obfuscation', () => {
44
it('encodes strings to base64', () => {
@@ -16,4 +16,14 @@ describe('obfuscation', () => {
1616
expect(decodeBase64(encodeBase64(regex))).toEqual(regex);
1717
});
1818
});
19+
20+
describe('salt', () => {
21+
it('converts from bytes to string and base64 string', () => {
22+
const chars = new Uint8Array([101, 112, 112, 111]); // eppo
23+
const eppoSalt = new Salt(chars);
24+
25+
expect(eppoSalt.saltString).toEqual('eppo');
26+
expect(eppoSalt.base64String).toEqual('ZXBwbw==');
27+
});
28+
});
1929
});

src/obfuscation.ts

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,8 @@ import * as SparkMD5 from 'spark-md5';
33

44
import { PrecomputedFlag } from './interfaces';
55

6-
export function getMD5Hash(input: string): string {
7-
return SparkMD5.hash(input);
8-
}
9-
10-
function saltedHasher(salt: string) {
11-
return (input: string) => getMD5Hash(salt + input);
6+
export function getMD5Hash(input: string, salt = ''): string {
7+
return new SparkMD5().append(salt).append(input).end();
128
}
139

1410
export function encodeBase64(input: string) {
@@ -24,7 +20,6 @@ export function obfuscatePrecomputedFlags(
2420
precomputedFlags: Record<string, PrecomputedFlag>,
2521
): Record<string, PrecomputedFlag> {
2622
const response: Record<string, PrecomputedFlag> = {};
27-
const hash = saltedHasher(salt);
2823

2924
Object.keys(precomputedFlags).map((flagKey) => {
3025
const assignment = precomputedFlags[flagKey];
@@ -34,7 +29,7 @@ export function obfuscatePrecomputedFlags(
3429
Object.entries(assignment.extraLogging).map((kvArr) => kvArr.map(encodeBase64)),
3530
);
3631

37-
const hashedKey = hash(flagKey);
32+
const hashedKey = getMD5Hash(flagKey, salt);
3833
response[hashedKey] = {
3934
flagKey: hashedKey,
4035
variationType: assignment.variationType,
@@ -48,30 +43,29 @@ export function obfuscatePrecomputedFlags(
4843
return response;
4944
}
5045

51-
export interface Salt {
46+
export interface ISalt {
5247
saltString: string;
5348
base64String: string;
5449
bytes: Uint8Array;
5550
}
5651

57-
let _saltOverride: Salt | null = null;
58-
export function setSaltOverrideForTests(salt: Salt | null) {
59-
_saltOverride = salt;
52+
export class Salt implements ISalt {
53+
public readonly saltString: string;
54+
public readonly base64String: string;
55+
constructor(public readonly bytes: Uint8Array) {
56+
this.saltString = String.fromCharCode(...bytes);
57+
this.base64String = encodeBase64(this.saltString);
58+
}
59+
}
60+
61+
let _saltOverride: ISalt | null = null;
62+
export function setSaltOverrideForTests(salt: Uint8Array | null) {
63+
_saltOverride = salt ? new Salt(salt) : null;
6064
}
6165

62-
export function generateSalt(length = 16): Salt {
66+
export function generateSalt(length = 16): ISalt {
6367
if (_saltOverride) return _saltOverride;
6468
const array = new Uint8Array(length);
6569
crypto.getRandomValues(array);
66-
67-
const saltString = Array.from(array)
68-
.map((byte) => byte.toString(16).padStart(2, '0'))
69-
.join('');
70-
const base64String = encodeBase64(String.fromCharCode(...array));
71-
72-
return {
73-
saltString,
74-
base64String,
75-
bytes: array,
76-
};
70+
return new Salt(array);
7771
}

0 commit comments

Comments
 (0)