Skip to content

Commit 3fa2541

Browse files
committed
Fix a bug where some browsers could not access subtleCrypto.digest when passed around directly
1 parent c6a08bc commit 3fa2541

File tree

3 files changed

+32
-28
lines changed

3 files changed

+32
-28
lines changed

src/telemetrydeck.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { version } from './utils/version.js';
1313
* @property {string} [salt] A salt to use when hashing the clientUser ID
1414
* @property {boolean} [testMode] If "true", signals will be marked as test signals and only show up in Test Mode in the Dashbaord
1515
* @property {Store} [store] A store to use for queueing signals
16-
* @property {Function} [cryptoDigest] A function to use for calculating the SHA-256 hash of the clientUser ID. Null to use the browser's built-in crypto.subtle.digest function.
16+
* @property {Function} [subtleCrypto] Used for providing an alternative implementation of SubtleCrypto where no browser is available. Expects a class providing a `.digest(method, value)` method.
1717
*/
1818

1919
export default class TelemetryDeck {
@@ -28,7 +28,7 @@ export default class TelemetryDeck {
2828
* @param {TelemetryDeckOptions} options
2929
*/
3030
constructor(options = {}) {
31-
const { target, appID, clientUser, sessionID, salt, testMode, store, cryptoDigest } = options;
31+
const { target, appID, clientUser, sessionID, salt, testMode, store, subtleCrypto } = options;
3232

3333
if (!appID) {
3434
throw new Error('appID is required');
@@ -41,7 +41,7 @@ export default class TelemetryDeck {
4141
this.sessionID = sessionID ?? randomString();
4242
this.salt = salt ?? this.salt;
4343
this.testMode = testMode ?? this.testMode;
44-
this.cryptoDigest = cryptoDigest;
44+
this.subtleCrypto = subtleCrypto;
4545
}
4646

4747
/**
@@ -95,7 +95,7 @@ export default class TelemetryDeck {
9595

9696
async _hashedClientUser(clientUser, salt) {
9797
if (clientUser + salt !== this._clientUserAndSalt) {
98-
this._clientUserHashed = await sha256([clientUser, salt].join(''), this.cryptoDigest);
98+
this._clientUserHashed = await sha256([clientUser, salt].join(''), this.subtleCrypto);
9999
this._clientUserAndSalt = clientUser + salt;
100100
}
101101

src/utils/sha256.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
/**
2-
* Calculate the SHA-256 hash of a string using a provided crypto digest function.
3-
* Defaults to globalThis.crypto.subtle.digest if available.
2+
* Calculate the SHA-256 hash of a string using a provided instance of SubtleCrypto
3+
* (e.g. window.crypto.subtle).
4+
*
5+
* Requires `.digest()` to be available on the SubtleCrypto instance.
6+
*
7+
* Defaults to globalThis.crypto.subtle if available.
48
*
59
* // https://stackoverflow.com/a/48161723/54547
610
*
7-
* @param {Function} cryptoDigest
11+
* @param {Function} subtleCrypto
812
* @param {string} message
913
* @returns {Promise<string>}
1014
*/
11-
export async function sha256(message, cryptoDigest = globalThis?.crypto?.subtle?.digest) {
15+
export async function sha256(message, subtleCrypto = globalThis?.crypto?.subtle) {
1216
// encode as UTF-8
1317
const messageBuffer = new TextEncoder().encode(message);
1418

1519
// hash the message
16-
const hashBuffer = await cryptoDigest('SHA-256', messageBuffer);
20+
const hashBuffer = await subtleCrypto.digest('SHA-256', messageBuffer);
1721

1822
// convert ArrayBuffer to Array
1923
const hashArray = [...new Uint8Array(hashBuffer)];

tests/sdk.test.js

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ test.beforeEach((t) => {
1616
const random = sinon.fake.returns(0.4); // chosen by fair dice roll. guaranteed to be random.
1717
sinon.replace(Math, 'random', random);
1818

19-
t.context.cryptoDigest = sinon.fake((_, value) => Promise.resolve(Buffer.from(value)));
19+
t.context.subtleCrypto = {
20+
digest: sinon.fake((_, value) => Promise.resolve(Buffer.from(value))),
21+
};
2022
});
2123

2224
test.afterEach.always(() => {
@@ -49,12 +51,12 @@ test.serial('Can pass optional user, target and testMode flag', (t) => {
4951
});
5052

5153
test.serial('Can send a signal', async (t) => {
52-
const { fake, cryptoDigest } = t.context;
54+
const { fake, subtleCrypto } = t.context;
5355

5456
const td = new TelemetryDeck({
5557
appID: 'foo',
5658
clientUser: 'anonymous',
57-
cryptoDigest,
59+
subtleCrypto,
5860
});
5961

6062
const response = await td.signal('test');
@@ -93,12 +95,12 @@ test.serial("Can't send a signal without a type", async (t) => {
9395
});
9496

9597
test.serial('Can send additional payload attributes', async (t) => {
96-
const { fake, cryptoDigest } = t.context;
98+
const { fake, subtleCrypto } = t.context;
9799

98100
const td = new TelemetryDeck({
99101
appID: 'foo',
100102
clientUser: 'anonymous',
101-
cryptoDigest,
103+
subtleCrypto,
102104
});
103105

104106
const response = await td.signal('test', {
@@ -132,13 +134,13 @@ test.serial('Can send additional payload attributes', async (t) => {
132134
});
133135

134136
test.serial('Can send a signal with salty user', async (t) => {
135-
const { fake, cryptoDigest } = t.context;
137+
const { fake, subtleCrypto } = t.context;
136138

137139
const td = new TelemetryDeck({
138140
appID: 'foo',
139141
clientUser: 'anonymous',
140142
salt: 'salty',
141-
cryptoDigest,
143+
subtleCrypto,
142144
});
143145

144146
const response = await td.signal('test');
@@ -165,13 +167,13 @@ test.serial('Can send a signal with salty user', async (t) => {
165167
});
166168

167169
test.serial('Can send a signal with sessionID', async (t) => {
168-
const { fake, cryptoDigest } = t.context;
170+
const { fake, subtleCrypto } = t.context;
169171

170172
const td = new TelemetryDeck({
171173
appID: 'foo',
172174
clientUser: 'anonymous',
173175
sessionID: '1234567890',
174-
cryptoDigest,
176+
subtleCrypto,
175177
});
176178

177179
const response = await td.signal('test');
@@ -200,13 +202,13 @@ test.serial('Can queue signals and send them later', async (t) => {
200202
advanceTimeDelta: 10,
201203
});
202204
const now = new Date();
203-
const { fake, cryptoDigest } = t.context;
205+
const { fake, subtleCrypto } = t.context;
204206

205207
const td = new TelemetryDeck({
206208
appID: 'foo',
207209
clientUser: 'anonymous',
208210
sessionID: '1234567890',
209-
cryptoDigest,
211+
subtleCrypto,
210212
});
211213

212214
await td.queue('foo');
@@ -277,13 +279,13 @@ test.serial('Can queue signals and send them later', async (t) => {
277279
});
278280

279281
test.serial('Can build signal payloads', async (t) => {
280-
const { fake, cryptoDigest } = t.context;
282+
const { fake, subtleCrypto } = t.context;
281283

282284
const td = new TelemetryDeck({
283285
appID: 'foo',
284286
clientUser: 'anonymous',
285287
sessionID: '1234567890',
286-
cryptoDigest,
288+
subtleCrypto,
287289
});
288290

289291
const response = await td.signal('test', {
@@ -324,9 +326,7 @@ test.serial('Can build signal payloads', async (t) => {
324326

325327
test.serial('Can find build-in crypto digest', async (t) => {
326328
globalThis.crypto = {
327-
subtle: {
328-
digest: t.context.cryptoDigest,
329-
},
329+
subtle: t.context.subtleCrypto,
330330
};
331331

332332
const td = new TelemetryDeck({
@@ -335,19 +335,19 @@ test.serial('Can find build-in crypto digest', async (t) => {
335335
});
336336

337337
await td.signal('test');
338-
t.is(t.context.cryptoDigest.callCount, 1);
338+
t.is(t.context.subtleCrypto.digest.callCount, 1);
339339

340340
delete globalThis.crypto;
341341
});
342342

343343
test.serial('Changing the salt also changes the hash', async (t) => {
344-
const { fake, cryptoDigest } = t.context;
344+
const { fake, subtleCrypto } = t.context;
345345

346346
const td = new TelemetryDeck({
347347
appID: 'foo',
348348
clientUser: 'anonymous',
349349
sessionID: '1234567890',
350-
cryptoDigest,
350+
subtleCrypto,
351351
});
352352

353353
await td.signal('test');

0 commit comments

Comments
 (0)