Skip to content

Commit

Permalink
Merge pull request #74 from IABTechLab/llp-UID2-3077-fix-EUID-key-check
Browse files Browse the repository at this point in the history
Fix public key check for EUID SDK.
  • Loading branch information
lionell-pack-ttd authored Mar 26, 2024
2 parents 1a476ed + 39c8d04 commit 1455720
Show file tree
Hide file tree
Showing 9 changed files with 56 additions and 38 deletions.
3 changes: 2 additions & 1 deletion src/apiClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ProductName, SdkBase } from './sdkBase';
import { SdkBase } from './sdkBase';
import { ProductName } from './product';
import { isValidIdentity, Identity } from './Identity';
import { CstgBox } from './cstgBox';
import { exportPublicKey } from './cstgCrypto';
Expand Down
7 changes: 5 additions & 2 deletions src/clientSideIdentityOptions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ProductName } from './product';

export type ClientSideIdentityOptions = {
readonly serverPublicKey: string;
readonly subscriptionId: string;
Expand All @@ -10,7 +12,8 @@ export function stripPublicKeyPrefix(serverPublicKey: string) {
}

export function isClientSideIdentityOptionsOrThrow(
maybeOpts: any
maybeOpts: any,
product: ProductName = 'UID2'
): maybeOpts is ClientSideIdentityOptions {
if (typeof maybeOpts !== 'object' || maybeOpts === null) {
throw new TypeError('opts must be an object');
Expand All @@ -20,7 +23,7 @@ export function isClientSideIdentityOptionsOrThrow(
if (typeof opts.serverPublicKey !== 'string') {
throw new TypeError('opts.serverPublicKey must be a string');
}
const serverPublicKeyPrefix = /^UID2-X-[A-Z]-.+/;
const serverPublicKeyPrefix = new RegExp(`^${product}-X-[A-Z]-.+`);
if (!serverPublicKeyPrefix.test(opts.serverPublicKey)) {
throw new TypeError(
`opts.serverPublicKey must match the regular expression ${serverPublicKeyPrefix}`
Expand Down
3 changes: 2 additions & 1 deletion src/euidSdk.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EventType, CallbackHandler } from './callbackManager';
import { CallbackContainer, ProductDetails, SdkBase, SDKSetup } from './sdkBase';
import { CallbackContainer, SdkBase, SDKSetup } from './sdkBase';
import { ProductDetails } from './product';

export * from './exports';

Expand Down
7 changes: 7 additions & 0 deletions src/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type ProductName = 'UID2' | 'EUID';
export type ProductDetails = {
name: ProductName;
cookieName: string;
localStorageKey: string;
defaultBaseUrl: string;
};
28 changes: 8 additions & 20 deletions src/sdkBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import {
ClientSideIdentityOptions,
isClientSideIdentityOptionsOrThrow,
} from './clientSideIdentityOptions';
import { isNormalizedPhone, normalizeEmail } from './diiNormalization';
import { normalizeEmail } from './diiNormalization';
import { isBase64Hash } from './hashedDii';
import { PromiseHandler } from './promiseHandler';
import { StorageManager } from './storageManager';
import { hashAndEncodeIdentifier } from './encoding/hash';
import { ProductDetails } from './product';

function hasExpired(expiry: number, now = Date.now()) {
return expiry <= now;
Expand All @@ -23,23 +24,15 @@ export type SDKSetup = {
};
export type CallbackContainer = { callback?: () => void };

export type ProductName = 'UID2' | 'EUID';
export type ProductDetails = {
name: ProductName;
cookieName: string;
localStorageKey: string;
defaultBaseUrl: string;
};

export abstract class SdkBase {
static get VERSION() {
return version;
}
static get DEFAULT_REFRESH_RETRY_PERIOD_MS() {
return 5000;
}
static IdentityStatus = IdentityStatus;
static EventType = EventType;
static readonly IdentityStatus = IdentityStatus;
static readonly EventType = EventType;

// Push functions to this array to receive event notifications
public callbacks: CallbackHandler[] = [];
Expand All @@ -54,20 +47,15 @@ export abstract class SdkBase {
private _apiClient: ApiClient | undefined;

// State
private _product: ProductDetails;
protected _product: ProductDetails;
private _opts: SdkOptions = {};
private _identity: Identity | OptoutIdentity | null | undefined;
private _initComplete = false;

// Sets up nearly everything, but does not run SdkLoaded callbacks - derived classes must run them.
protected constructor(
existingCallbacks: CallbackHandler[] | undefined = undefined,
product: ProductDetails
) {
protected constructor(existingCallbacks: CallbackHandler[] | undefined, product: ProductDetails) {
this._product = product;
this._logger = MakeLogger(console, product.name);
const exception = new Error();
this._logger.log(`Constructing an SDK!`, exception.stack);
if (existingCallbacks) this.callbacks = existingCallbacks;

this._tokenPromiseHandler = new PromiseHandler(this);
Expand All @@ -90,7 +78,7 @@ export abstract class SdkBase {
public async setIdentityFromEmail(email: string, opts: ClientSideIdentityOptions) {
this._logger.log('Sending request', email);
this.throwIfInitNotComplete('Cannot set identity before calling init.');
isClientSideIdentityOptionsOrThrow(opts);
isClientSideIdentityOptionsOrThrow(opts, this._product.name);

const normalizedEmail = normalizeEmail(email);
if (normalizedEmail === undefined) {
Expand All @@ -103,7 +91,7 @@ export abstract class SdkBase {

public async setIdentityFromEmailHash(emailHash: string, opts: ClientSideIdentityOptions) {
this.throwIfInitNotComplete('Cannot set identity before calling init.');
isClientSideIdentityOptionsOrThrow(opts);
isClientSideIdentityOptionsOrThrow(opts, this._product.name);

if (!isBase64Hash(emailHash)) {
throw new Error('Invalid hash');
Expand Down
7 changes: 4 additions & 3 deletions src/uid2Sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
import { isNormalizedPhone, normalizeEmail } from './diiNormalization';
import { isBase64Hash } from './hashedDii';
import { hashAndEncodeIdentifier, hashIdentifier } from './encoding/hash';
import { CallbackContainer, ProductDetails, SdkBase, SDKSetup } from './sdkBase';
import { CallbackContainer, SdkBase, SDKSetup } from './sdkBase';
import { ProductDetails } from './product';

export * from './exports';

Expand Down Expand Up @@ -73,7 +74,7 @@ export class UID2 extends SdkBase {

public async setIdentityFromPhone(phone: string, opts: ClientSideIdentityOptions) {
this.throwIfInitNotComplete('Cannot set identity before calling init.');
isClientSideIdentityOptionsOrThrow(opts);
isClientSideIdentityOptionsOrThrow(opts, this._product.name);

if (!isNormalizedPhone(phone)) {
throw new Error('Invalid phone number');
Expand All @@ -85,7 +86,7 @@ export class UID2 extends SdkBase {

public async setIdentityFromPhoneHash(phoneHash: string, opts: ClientSideIdentityOptions) {
this.throwIfInitNotComplete('Cannot set identity before calling init.');
isClientSideIdentityOptionsOrThrow(opts);
isClientSideIdentityOptionsOrThrow(opts, this._product.name);

if (!isBase64Hash(phoneHash)) {
throw new Error('Invalid hash');
Expand Down
4 changes: 1 addition & 3 deletions src/unitTests/uid2CstgBox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { NAME_CURVE, decryptClientRequest, encryptServerMessage, makeIdentityV2
import { bytesToBase64 } from '../encoding/base64';
import { CstgBox } from '../cstgBox';
import { exportPublicKey } from '../cstgCrypto';

const CryptoKey = require('crypto').webcrypto.CryptoKey;
import * as crypto from 'crypto';

describe('UID2CstgBox', () => {
let serverPublicKey: ArrayBuffer;
Expand Down Expand Up @@ -54,7 +53,6 @@ describe('UID2CstgBox', () => {
false,
[]
);
expect(importedPublicKey).toBeInstanceOf(CryptoKey);
expect(importedPublicKey.algorithm).toEqual({
name: 'ECDH',
namedCurve: NAME_CURVE,
Expand Down
11 changes: 8 additions & 3 deletions src/unitTests/uid2CstgCrypto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const CryptoKey = require('crypto').webcrypto.CryptoKey;
const generateSharedKey = (keyUsages: KeyUsage[]) => {
return crypto.subtle.generateKey({ name: 'AES-GCM', length: 128 }, true, keyUsages);
};
const expectedAlgorithm = {
name: 'ECDH',
namedCurve: 'P-256',
};

const sharedKeyIsMatched = async (sharedKey1: CryptoKey, sharedKey2: CryptoKey) => {
const testMessage = 'Test secret message';
Expand Down Expand Up @@ -149,8 +153,9 @@ describe('uid2CstgCrypto Tests', () => {
describe('#generateKeyPair', () => {
it(' should return a valid CryptoKePair', async () => {
const keyPair = await generateKeyPair('P-256');
expect(keyPair.publicKey).toBeInstanceOf(CryptoKey);
expect(keyPair.privateKey).toBeInstanceOf(CryptoKey);

expect(keyPair.publicKey.algorithm).toEqual(expectedAlgorithm);
expect(keyPair.privateKey.algorithm).toEqual(expectedAlgorithm);
});
});

Expand Down Expand Up @@ -197,6 +202,6 @@ describe('uid2CstgCrypto Tests', () => {

const importedKey = await importPublicKey(bytesToBase64(new Uint8Array(publicKey)), 'P-256');

expect(importedKey).toBeInstanceOf(CryptoKey);
expect(importedKey.algorithm).toEqual(expectedAlgorithm);
});
});
24 changes: 19 additions & 5 deletions src/unitTests/uid2Sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,43 @@ describe('#uid2Sdk', () => {

describe('#isClientSideIdentityOptionsOrThrow', () => {
test('should throw opts must be an object error when config is not object', () => {
expect(() => isClientSideIdentityOptionsOrThrow('')).toThrow('opts must be an object');
expect(() => isClientSideIdentityOptionsOrThrow('', 'UID2')).toThrow(
'opts must be an object'
);
});
test('should throw serverPublicKey must be a string error when serverPublicKey is not a string', () => {
expect(() =>
isClientSideIdentityOptionsOrThrow(makeCstgOption({ serverPublicKey: {} }))
isClientSideIdentityOptionsOrThrow(makeCstgOption({ serverPublicKey: {} }), 'UID2')
).toThrow('opts.serverPublicKey must be a string');
});
test('should throw serverPublicKey prefix when serverPublicKey is invalid', () => {
expect(() =>
isClientSideIdentityOptionsOrThrow(
makeCstgOption({ serverPublicKey: 'test-server-public-key' })
makeCstgOption({ serverPublicKey: 'test-server-public-key' }),
'UID2'
)
).toThrow('opts.serverPublicKey must match the regular expression /^UID2-X-[A-Z]-.+/');
});
test('should throw serverPublicKey prefix (EUID) when serverPublicKey is invalid', () => {
expect(() =>
isClientSideIdentityOptionsOrThrow(
makeCstgOption({ serverPublicKey: 'test-server-public-key' }),
'EUID'
)
).toThrow('opts.serverPublicKey must match the regular expression /^EUID-X-[A-Z]-.+/');
});
test('should throw subscriptionId must be a string error when subscriptionId is not a string', () => {
expect(() =>
isClientSideIdentityOptionsOrThrow(makeCstgOption({ subscriptionId: {} }))
isClientSideIdentityOptionsOrThrow(makeCstgOption({ subscriptionId: {} }), 'UID2')
).toThrow('opts.subscriptionId must be a string');
});
test('should throw subscriptionId is empty error when subscriptionId is not given', () => {
expect(() =>
isClientSideIdentityOptionsOrThrow(makeCstgOption({ subscriptionId: '' }))
isClientSideIdentityOptionsOrThrow(makeCstgOption({ subscriptionId: '' }), 'UID2')
).toThrow('opts.subscriptionId is empty');
});
test('should succeed when given a valid object', () => {
expect(isClientSideIdentityOptionsOrThrow(makeCstgOption())).toBe(true);
});
});
});

0 comments on commit 1455720

Please sign in to comment.