From db5b08bd0f7654297478cf21eaa9fb24d2247778 Mon Sep 17 00:00:00 2001 From: addisonbeck Date: Tue, 12 Aug 2025 15:09:02 -0400 Subject: [PATCH] refactor: introduce @bitwarden/encoding --- .github/CODEOWNERS | 1 + libs/common/src/platform/misc/utils.ts | 17 +++ libs/encoding/README.md | 5 + libs/encoding/eslint.config.mjs | 3 + libs/encoding/jest.config.js | 10 ++ libs/encoding/package.json | 11 ++ libs/encoding/project.json | 33 +++++ libs/encoding/src/encoding.spec.ts | 8 ++ libs/encoding/src/index.ts | 180 +++++++++++++++++++++++++ libs/encoding/tsconfig.eslint.json | 6 + libs/encoding/tsconfig.json | 13 ++ libs/encoding/tsconfig.lib.json | 10 ++ libs/encoding/tsconfig.spec.json | 10 ++ package-lock.json | 9 ++ tsconfig.base.json | 1 + 15 files changed, 317 insertions(+) create mode 100644 libs/encoding/README.md create mode 100644 libs/encoding/eslint.config.mjs create mode 100644 libs/encoding/jest.config.js create mode 100644 libs/encoding/package.json create mode 100644 libs/encoding/project.json create mode 100644 libs/encoding/src/encoding.spec.ts create mode 100644 libs/encoding/src/index.ts create mode 100644 libs/encoding/tsconfig.eslint.json create mode 100644 libs/encoding/tsconfig.json create mode 100644 libs/encoding/tsconfig.lib.json create mode 100644 libs/encoding/tsconfig.spec.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c6cb1c5689f..e986e0214d1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -103,6 +103,7 @@ libs/core-test-utils @bitwarden/team-platform-dev libs/state @bitwarden/team-platform-dev libs/state-test-utils @bitwarden/team-platform-dev libs/device-type @bitwarden/team-platform-dev +libs/encoding @bitwarden/team-platform-dev # Web utils used across app and connectors apps/web/src/utils/ @bitwarden/team-platform-dev # Web core and shared files diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index c103e346a85..9ecbd71eda8 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -71,6 +71,7 @@ export class Utils { } } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromB64ToArray(str: string): Uint8Array { if (str == null) { return null; @@ -88,10 +89,12 @@ export class Utils { } } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromUrlB64ToArray(str: string): Uint8Array { return Utils.fromB64ToArray(Utils.fromUrlB64ToB64(str)); } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromHexToArray(str: string): Uint8Array { if (Utils.isNode) { return new Uint8Array(Buffer.from(str, "hex")); @@ -104,6 +107,7 @@ export class Utils { } } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromUtf8ToArray(str: string): Uint8Array { if (Utils.isNode) { return new Uint8Array(Buffer.from(str, "utf8")); @@ -117,6 +121,7 @@ export class Utils { } } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromByteStringToArray(str: string): Uint8Array { if (str == null) { return null; @@ -128,6 +133,7 @@ export class Utils { return arr; } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromBufferToB64(buffer: ArrayBuffer): string { if (buffer == null) { return null; @@ -144,23 +150,28 @@ export class Utils { } } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromBufferToUrlB64(buffer: ArrayBuffer): string { return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer)); } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromB64toUrlB64(b64Str: string) { return b64Str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromBufferToUtf8(buffer: ArrayBuffer): string { return BufferLib.from(buffer).toString("utf8"); } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromBufferToByteString(buffer: ArrayBuffer): string { return String.fromCharCode.apply(null, new Uint8Array(buffer)); } // ref: https://stackoverflow.com/a/40031979/1090359 + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromBufferToHex(buffer: ArrayBuffer): string { if (Utils.isNode) { return Buffer.from(buffer).toString("hex"); @@ -179,6 +190,7 @@ export class Utils { * @param {string} hexString - A string of hexadecimal characters. * @returns {ArrayBuffer} The ArrayBuffer representation of the hex string. */ + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static hexStringToArrayBuffer(hexString: string): ArrayBuffer { // Check if the hexString has an even length, as each hex digit represents half a byte (4 bits), // and it takes two hex digits to represent a full byte (8 bits). @@ -209,6 +221,7 @@ export class Utils { return arrayBuffer; } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromUrlB64ToB64(urlB64Str: string): string { let output = urlB64Str.replace(/-/g, "+").replace(/_/g, "/"); switch (output.length % 4) { @@ -227,10 +240,12 @@ export class Utils { return output; } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromUrlB64ToUtf8(urlB64Str: string): string { return Utils.fromB64ToUtf8(Utils.fromUrlB64ToB64(urlB64Str)); } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromUtf8ToB64(utfStr: string): string { if (Utils.isNode) { return Buffer.from(utfStr, "utf8").toString("base64"); @@ -239,10 +254,12 @@ export class Utils { } } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromUtf8ToUrlB64(utfStr: string): string { return Utils.fromBufferToUrlB64(Utils.fromUtf8ToArray(utfStr)); } + /** @deprecated Moved to @bitwarden/encoding as a directly exported function*/ static fromB64ToUtf8(b64Str: string): string { if (Utils.isNode) { return Buffer.from(b64Str, "base64").toString("utf8"); diff --git a/libs/encoding/README.md b/libs/encoding/README.md new file mode 100644 index 00000000000..a5ef938e4a2 --- /dev/null +++ b/libs/encoding/README.md @@ -0,0 +1,5 @@ +# encoding + +Owned by: platform + +Encoding helpers diff --git a/libs/encoding/eslint.config.mjs b/libs/encoding/eslint.config.mjs new file mode 100644 index 00000000000..9c37d10e3ff --- /dev/null +++ b/libs/encoding/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from "../../eslint.config.mjs"; + +export default [...baseConfig]; diff --git a/libs/encoding/jest.config.js b/libs/encoding/jest.config.js new file mode 100644 index 00000000000..20714dba45c --- /dev/null +++ b/libs/encoding/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + displayName: "encoding", + preset: "../../jest.preset.js", + testEnvironment: "node", + transform: { + "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], + }, + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "../../coverage/libs/encoding", +}; diff --git a/libs/encoding/package.json b/libs/encoding/package.json new file mode 100644 index 00000000000..39752a5f055 --- /dev/null +++ b/libs/encoding/package.json @@ -0,0 +1,11 @@ +{ + "name": "@bitwarden/encoding", + "version": "0.0.1", + "description": "Encoding helpers", + "private": true, + "type": "commonjs", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "license": "GPL-3.0", + "author": "platform" +} diff --git a/libs/encoding/project.json b/libs/encoding/project.json new file mode 100644 index 00000000000..6bfe91ced71 --- /dev/null +++ b/libs/encoding/project.json @@ -0,0 +1,33 @@ +{ + "name": "encoding", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/encoding/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/encoding", + "main": "libs/encoding/src/index.ts", + "tsConfig": "libs/encoding/tsconfig.lib.json", + "assets": ["libs/encoding/*.md"] + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/encoding/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/encoding/jest.config.js" + } + } + } +} diff --git a/libs/encoding/src/encoding.spec.ts b/libs/encoding/src/encoding.spec.ts new file mode 100644 index 00000000000..ab9d6c9e0b0 --- /dev/null +++ b/libs/encoding/src/encoding.spec.ts @@ -0,0 +1,8 @@ +import * as lib from "./index"; + +describe("encoding", () => { + // This test will fail until something is exported from index.ts + it("should work", () => { + expect(lib).toBeDefined(); + }); +}); diff --git a/libs/encoding/src/index.ts b/libs/encoding/src/index.ts new file mode 100644 index 00000000000..ed66bbef9d7 --- /dev/null +++ b/libs/encoding/src/index.ts @@ -0,0 +1,180 @@ +import { Buffer as BufferLib } from "buffer/"; + +const isNode = typeof (globalThis as any)?.process?.versions?.node === "string"; +// We cast globalThis to any to keep typescript happy +const g: any = globalThis as any; + +export function fromB64ToArray(str: string): Uint8Array | null { + if (str == null) { + return null; + } + if (isNode) { + return new Uint8Array(Buffer.from(str, "base64")); + } else { + const binaryString = g.atob(str); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } +} + +export function fromUrlB64ToArray(str: string): Uint8Array | null { + return fromB64ToArray(fromUrlB64ToB64(str)); +} + +export function fromHexToArray(str: string): Uint8Array { + if (isNode) { + return new Uint8Array(Buffer.from(str, "hex")); + } else { + const bytes = new Uint8Array(str.length / 2); + for (let i = 0; i < str.length; i += 2) { + bytes[i / 2] = parseInt(str.substr(i, 2), 16); + } + return bytes; + } +} + +export function fromUtf8ToArray(str: string): Uint8Array { + if (isNode) { + return new Uint8Array(Buffer.from(str, "utf8")); + } else { + const strUtf8 = unescape(encodeURIComponent(str)); + const arr = new Uint8Array(strUtf8.length); + for (let i = 0; i < strUtf8.length; i++) { + arr[i] = strUtf8.charCodeAt(i); + } + return arr; + } +} + +export function fromByteStringToArray(str: string): Uint8Array | null { + if (str == null) { + return null; + } + const arr = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + arr[i] = str.charCodeAt(i); + } + return arr; +} + +export function fromBufferToB64(buffer: ArrayBuffer): string | null { + if (buffer == null) { + return null; + } + if (isNode) { + return Buffer.from(buffer).toString("base64"); + } else { + let binary = ""; + const bytes = new Uint8Array(buffer); + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return g.btoa(binary); + } +} + +export function fromBufferToUrlB64(buffer: ArrayBuffer): string | null { + const b64 = fromBufferToB64(buffer); + return b64 == null ? null : fromB64toUrlB64(b64); +} + +export function fromB64toUrlB64(b64Str: string): string { + return b64Str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); +} + +export function fromBufferToUtf8(buffer: ArrayBuffer): string { + return BufferLib.from(buffer).toString("utf8"); +} + +export function fromBufferToByteString(buffer: ArrayBuffer): string { + return String.fromCharCode.apply(null, new Uint8Array(buffer) as unknown as number[]); +} + +// ref: https://stackoverflow.com/a/40031979/1090359 +export function fromBufferToHex(buffer: ArrayBuffer): string { + if (isNode) { + return Buffer.from(buffer).toString("hex"); + } else { + const bytes = new Uint8Array(buffer); + return Array.prototype.map + .call(bytes, (x: number) => ("00" + x.toString(16)).slice(-2)) + .join(""); + } +} + +/** + * Converts a hex string to an ArrayBuffer. + * Note: this doesn't need any Node specific code as parseInt() / ArrayBuffer / Uint8Array + * work the same in Node and the browser. + * @param {string} hexString - A string of hexadecimal characters. + * @returns {ArrayBuffer} The ArrayBuffer representation of the hex string. + */ +export function hexStringToArrayBuffer(hexString: string): ArrayBuffer { + // Check if the hexString has an even length, as each hex digit represents half a byte (4 bits), + // and it takes two hex digits to represent a full byte (8 bits). + if (hexString.length % 2 !== 0) { + throw "HexString has to be an even length"; + } + + // Create an ArrayBuffer with a length that is half the length of the hex string, + // because each pair of hex digits will become a single byte. + const arrayBuffer = new ArrayBuffer(hexString.length / 2); + + // Create a Uint8Array view on top of the ArrayBuffer (each position represents a byte) + // as ArrayBuffers cannot be edited directly. + const uint8Array = new Uint8Array(arrayBuffer); + + // Loop through the bytes + for (let i = 0; i < uint8Array.length; i++) { + // Extract two hex characters (1 byte) + const hexByte = hexString.substr(i * 2, 2); + + // Convert hexByte into a decimal value from base 16. (ex: ff --> 255) + const byteValue = parseInt(hexByte, 16); + + // Place the byte value into the uint8Array + uint8Array[i] = byteValue; + } + + return arrayBuffer; +} + +export function fromUrlB64ToB64(urlB64Str: string): string { + let output = urlB64Str.replace(/-/g, "+").replace(/_/g, "/"); + switch (output.length % 4) { + case 0: + break; + case 2: + output += "=="; + break; + case 3: + output += "="; + break; + default: + throw new Error("Illegal base64url string!"); + } + return output; +} + +export function fromUtf8ToB64(utfStr: string): string { + if (isNode) { + return Buffer.from(utfStr, "utf8").toString("base64"); + } else { + return BufferLib.from(utfStr, "utf8").toString("base64"); + } +} + +export function fromUtf8ToUrlB64(utfStr: string): string | null { + return fromBufferToUrlB64(fromUtf8ToArray(utfStr)); +} + +export function fromB64ToUtf8(b64Str: string): string { + if (isNode) { + return Buffer.from(b64Str, "base64").toString("utf8"); + } else { + return BufferLib.from(b64Str, "base64").toString("utf8"); + } +} diff --git a/libs/encoding/tsconfig.eslint.json b/libs/encoding/tsconfig.eslint.json new file mode 100644 index 00000000000..3daf120441a --- /dev/null +++ b/libs/encoding/tsconfig.eslint.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": ["src/**/*.ts", "src/**/*.js"], + "exclude": ["**/build", "**/dist"] +} diff --git a/libs/encoding/tsconfig.json b/libs/encoding/tsconfig.json new file mode 100644 index 00000000000..62ebbd94647 --- /dev/null +++ b/libs/encoding/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/encoding/tsconfig.lib.json b/libs/encoding/tsconfig.lib.json new file mode 100644 index 00000000000..9cbf6736007 --- /dev/null +++ b/libs/encoding/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.js", "src/**/*.spec.ts"] +} diff --git a/libs/encoding/tsconfig.spec.json b/libs/encoding/tsconfig.spec.json new file mode 100644 index 00000000000..1275f148a18 --- /dev/null +++ b/libs/encoding/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/package-lock.json b/package-lock.json index a24a9fd2aa9..5c6e44eac0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -347,6 +347,11 @@ "version": "0.0.0", "license": "GPL-3.0" }, + "libs/encoding": { + "name": "@bitwarden/encoding", + "version": "0.0.1", + "license": "GPL-3.0" + }, "libs/guid": { "name": "@bitwarden/guid", "version": "0.0.1", @@ -4612,6 +4617,10 @@ "resolved": "libs/dirt/card", "link": true }, + "node_modules/@bitwarden/encoding": { + "resolved": "libs/encoding", + "link": true + }, "node_modules/@bitwarden/generator-components": { "resolved": "libs/tools/generator/components", "link": true diff --git a/tsconfig.base.json b/tsconfig.base.json index 7379b940052..93bc31a38da 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -31,6 +31,7 @@ "@bitwarden/core-test-utils": ["libs/core-test-utils/src/index.ts"], "@bitwarden/device-type": ["libs/device-type/src/index.ts"], "@bitwarden/dirt-card": ["./libs/dirt/card/src"], + "@bitwarden/encoding": ["libs/encoding/src/index.ts"], "@bitwarden/generator-components": ["./libs/tools/generator/components/src"], "@bitwarden/generator-core": ["./libs/tools/generator/core/src"], "@bitwarden/generator-history": ["./libs/tools/generator/extensions/history/src"],