diff --git a/common/changes/@snowplow/browser-tracker-core/issue-1464-replace-sha1-buffer-polyfill_2026-03-29-16-18.json b/common/changes/@snowplow/browser-tracker-core/issue-1464-replace-sha1-buffer-polyfill_2026-03-29-16-18.json new file mode 100644 index 000000000..6ec66b2ac --- /dev/null +++ b/common/changes/@snowplow/browser-tracker-core/issue-1464-replace-sha1-buffer-polyfill_2026-03-29-16-18.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "comment": "Replace SHA1 buffer polyfill with lightweight implementation", + "type": "none", + "packageName": "@snowplow/browser-tracker-core" + } + ], + "packageName": "@snowplow/browser-tracker-core" +} \ No newline at end of file diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index feadd0224..f157b3ec4 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -222,10 +222,6 @@ "name": "@types/react", "allowedCategories": [ "trackers" ] }, - { - "name": "@types/sha1", - "allowedCategories": [ "libraries" ] - }, { "name": "@types/sinon", "allowedCategories": [ "trackers" ] @@ -430,10 +426,6 @@ "name": "saucelabs", "allowedCategories": [ "trackers" ] }, - { - "name": "sha1", - "allowedCategories": [ "libraries" ] - }, { "name": "sinon", "allowedCategories": [ "trackers" ] diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 72f87516b..44f6cc7f6 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -15,9 +15,6 @@ importers: '@snowplow/tracker-core': specifier: workspace:* version: link:../tracker-core - sha1: - specifier: ^1.1.1 - version: 1.1.1 tslib: specifier: ^2.3.1 version: 2.8.1 @@ -43,9 +40,6 @@ importers: '@types/jsdom': specifier: ~16.2.14 version: 16.2.15 - '@types/sha1': - specifier: ~1.1.3 - version: 1.1.5 '@types/uuid': specifier: ^10.0.0 version: 10.0.0 @@ -4061,9 +4055,6 @@ packages: '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - '@types/sha1@1.1.5': - resolution: {integrity: sha512-eE1PzjW7u2VfxI+bTsvjzjBfpwqvxSpgfUmnRNVY+PJU1NBsdGZlaO/qnVnPKHzzpgIl9YyBIxvrgBvt1mzt2A==} - '@types/split2@4.2.3': resolution: {integrity: sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw==} @@ -4779,9 +4770,6 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - charenc@0.0.2: - resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} - chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -5066,9 +5054,6 @@ packages: resolution: {integrity: sha512-mpjkSErNO6vioL/Cde2aF4UBysPFEMyn+1AN1t7Oc4yqvzSRWe8iBte4P8BHyjo64OmC+ZBxwjIqmpSpIWiQ7Q==} engines: {node: '>=10.0.0'} - crypt@0.0.2: - resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} - css-shorthand-properties@1.1.2: resolution: {integrity: sha512-C2AugXIpRGQTxaCW0N7n5jD/p5irUmCrwl03TrnMFBHDbdq44CFWR2zO7rK9xPN4Eo3pUxC4vQzQgbIpzrD1PQ==} @@ -8266,9 +8251,6 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sha1@1.1.1: - resolution: {integrity: sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==} - shallow-clone@3.0.1: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} @@ -11164,10 +11146,6 @@ snapshots: '@types/semver@7.5.8': {} - '@types/sha1@1.1.5': - dependencies: - '@types/node': 14.6.4 - '@types/split2@4.2.3': dependencies: '@types/node': 14.6.4 @@ -12190,8 +12168,6 @@ snapshots: chardet@0.7.0: {} - charenc@0.0.2: {} - chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -12492,8 +12468,6 @@ snapshots: dependencies: '@types/node': 16.18.122 - crypt@0.0.2: {} - css-shorthand-properties@1.1.2: {} css-value@0.0.1: {} @@ -16497,11 +16471,6 @@ snapshots: setprototypeof@1.2.0: {} - sha1@1.1.1: - dependencies: - charenc: 0.0.2 - crypt: 0.0.2 - shallow-clone@3.0.1: dependencies: kind-of: 6.0.3 diff --git a/common/config/rush/repo-state.json b/common/config/rush/repo-state.json index 6ed643312..407b63331 100644 --- a/common/config/rush/repo-state.json +++ b/common/config/rush/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "d3b2991182c4ca5879833e9c94d65eb10cb6c7f1", + "pnpmShrinkwrapHash": "7bfb995166f0ecc4b03f50797de4b75379120fb5", "preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f" } diff --git a/libraries/browser-tracker-core/package.json b/libraries/browser-tracker-core/package.json index ba000c14a..2efdf3419 100644 --- a/libraries/browser-tracker-core/package.json +++ b/libraries/browser-tracker-core/package.json @@ -23,7 +23,6 @@ }, "dependencies": { "@snowplow/tracker-core": "workspace:*", - "sha1": "^1.1.1", "tslib": "^2.3.1", "uuid": "^10.0.0" }, @@ -33,7 +32,6 @@ "@rollup/plugin-node-resolve": "~13.1.3", "@types/jest": "~28.1.1", "@types/jsdom": "~16.2.14", - "@types/sha1": "~1.1.3", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "~5.15.0", "@typescript-eslint/parser": "~5.15.0", diff --git a/libraries/browser-tracker-core/src/helpers/sha1.ts b/libraries/browser-tracker-core/src/helpers/sha1.ts new file mode 100644 index 000000000..af6a8c5ea --- /dev/null +++ b/libraries/browser-tracker-core/src/helpers/sha1.ts @@ -0,0 +1,98 @@ +/** + * Pure-JS SHA-1 implementation (FIPS 180-4). + * Replaces the `sha1` npm package to avoid pulling in a Node.js `Buffer` + * polyfill (~28 KB) when bundled for the browser. + */ +/** Compute SHA-1 hex digest of a UTF-8 string (FIPS 180-4). */ +export function sha1(message: string): string { + // Encode UTF-8 + const bytes: number[] = []; + for (let i = 0; i < message.length; i++) { + let c = message.charCodeAt(i); + if (c < 0x80) { + bytes.push(c); + } else if (c < 0x800) { + bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); + } else if (c >= 0xd800 && c <= 0xdbff && i + 1 < message.length) { + const next = message.charCodeAt(++i); + c = 0x10000 + ((c & 0x3ff) << 10) + (next & 0x3ff); + bytes.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); + } else { + bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); + } + } + + // Pre-processing: pad to 512-bit blocks + const bitLen = bytes.length * 8; + bytes.push(0x80); + while (bytes.length % 64 !== 56) bytes.push(0); + // Append length as 64-bit big-endian (high 32 bits always 0 for strings < 512 MB) + bytes.push(0, 0, 0, 0); + bytes.push((bitLen >>> 24) & 0xff, (bitLen >>> 16) & 0xff, (bitLen >>> 8) & 0xff, bitLen & 0xff); + + // Initialize hash values + let h0 = 0x67452301; + let h1 = 0xefcdab89; + let h2 = 0x98badcfe; + let h3 = 0x10325476; + let h4 = 0xc3d2e1f0; + + // Process each 512-bit block + const w = new Array(80); + for (let offset = 0; offset < bytes.length; offset += 64) { + for (let i = 0; i < 16; i++) { + w[i] = + (bytes[offset + i * 4] << 24) | + (bytes[offset + i * 4 + 1] << 16) | + (bytes[offset + i * 4 + 2] << 8) | + bytes[offset + i * 4 + 3]; + } + for (let i = 16; i < 80; i++) { + const n = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]; + w[i] = (n << 1) | (n >>> 31); + } + + let a = h0, + b = h1, + c = h2, + d = h3, + e = h4; + + for (let i = 0; i < 80; i++) { + let f: number, k: number; + if (i < 20) { + f = (b & c) | (~b & d); + k = 0x5a827999; + } else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ed9eba1; + } else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8f1bbcdc; + } else { + f = b ^ c ^ d; + k = 0xca62c1d6; + } + const temp = (((a << 5) | (a >>> 27)) + f + e + k + w[i]) | 0; + e = d; + d = c; + c = (b << 30) | (b >>> 2); + b = a; + a = temp; + } + + h0 = (h0 + a) | 0; + h1 = (h1 + b) | 0; + h2 = (h2 + c) | 0; + h3 = (h3 + d) | 0; + h4 = (h4 + e) | 0; + } + + // Produce hex digest + let hex = ''; + for (const h of [h0, h1, h2, h3, h4]) { + const part = (h >>> 0).toString(16); + hex += (part.length < 8 ? '00000000'.slice(part.length) : '') + part; + } + return hex; +} diff --git a/libraries/browser-tracker-core/src/tracker/index.ts b/libraries/browser-tracker-core/src/tracker/index.ts index 4bd868d8d..7d4b8cac4 100755 --- a/libraries/browser-tracker-core/src/tracker/index.ts +++ b/libraries/browser-tracker-core/src/tracker/index.ts @@ -7,7 +7,7 @@ import { SelfDescribingJson, LOG, } from '@snowplow/tracker-core'; -import hash from 'sha1'; +import { sha1 as hash } from '../helpers/sha1'; import { v4 as uuid } from 'uuid'; import { decorateQuerystring, diff --git a/libraries/browser-tracker-core/test/helpers/sha1.test.ts b/libraries/browser-tracker-core/test/helpers/sha1.test.ts new file mode 100644 index 000000000..291ee99a5 --- /dev/null +++ b/libraries/browser-tracker-core/test/helpers/sha1.test.ts @@ -0,0 +1,37 @@ +import { sha1 } from '../../src/helpers/sha1'; + +describe('sha1', () => { + // RFC 3174 test vectors + test('empty string', () => { + expect(sha1('')).toBe('da39a3ee5e6b4b0d3255bfef95601890afd80709'); + }); + + test('"abc"', () => { + expect(sha1('abc')).toBe('a9993e364706816aba3e25717850c26c9cd0d89d'); + }); + + test('"The quick brown fox jumps over the lazy dog"', () => { + expect(sha1('The quick brown fox jumps over the lazy dog')).toBe('2fd4e1c67a2d28fced849ee1bb76e7391b93eb12'); + }); + + test('multi-block message', () => { + expect(sha1('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')).toBe( + '84983e441c3bd26ebaae4aa1f95129e5e54670f1' + ); + }); + + // Domain hash use case — must match output of sha1 npm package v1.1.1 + test('domain hash: "example.com/"', () => { + expect(sha1('example.com/')).toBe('880970443b82bdca0439e34c62e6c667277c2b39'); + expect(sha1('example.com/').slice(0, 4)).toBe('8809'); + }); + + test('domain hash: "localhost/"', () => { + expect(sha1('localhost/')).toBe('1fffd42e9a20211889ebfae87a84665b392c19a4'); + expect(sha1('localhost/').slice(0, 4)).toBe('1fff'); + }); + + test('returns 40-char lowercase hex string', () => { + expect(sha1('anything')).toMatch(/^[0-9a-f]{40}$/); + }); +});