Skip to content

Commit

Permalink
refactor: reduce dependency on node internals (#7394)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone authored Nov 12, 2024
1 parent 4ab0af5 commit 9703274
Show file tree
Hide file tree
Showing 38 changed files with 343 additions and 144 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/test-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ jobs:
node -e 'assert.equal(require("zwave-js").libVersion, require("zwave-js/package.json").version)'
node -e 'assert.equal(require("zwave-js").libName, require("zwave-js/package.json").name)'
echo "Doing spot checks for some exports"
node -e 'assert.equal(typeof require("@zwave-js/core").randomBytes, "function")'
# Test if bundling for the browser is supported yet
- name: Are we browser yet?
continue-on-error: true
Expand Down
10 changes: 5 additions & 5 deletions docs/api/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ interface FirmwareUpdateResult {
The library includes helper methods (exported from `zwave-js/Utils`) to prepare the firmware update.

```ts
extractFirmware(rawData: Buffer, format: FirmwareFileFormat): Firmware
async extractFirmwareAsync(rawData: Buffer, format: FirmwareFileFormat): Promise<Firmware>
```

`rawData` is a buffer containing the original firmware update file, `format` describes which kind of file that is. The following formats are available:
Expand All @@ -394,7 +394,7 @@ extractFirmware(rawData: Buffer, format: FirmwareFileFormat): Firmware
- `"hec"` - An encrypted Intel HEX firmware file
- `"gecko"` - A binary gecko bootloader firmware file with `.gbl` extension

If successful, `extractFirmware` returns an `Firmware` object which can be passed to the `updateFirmware` method.
If successful, `extractFirmwareAsync` returns an `Firmware` object which can be passed to the `updateFirmware` method.

If no firmware data can be extracted, the method will throw.

Expand All @@ -417,7 +417,7 @@ Example usage:
let actualFirmware: Firmware;
try {
const format = guessFirmwareFileFormat(filename, rawData);
actualFirmware = extractFirmware(rawData, format);
actualFirmware = await extractFirmwareAsync(rawData, format);
} catch (e) {
// handle the error, then abort the update
}
Expand Down Expand Up @@ -455,7 +455,7 @@ If the given ZIP archive contains a compatible firmware update file, the method

Otherwise `undefined` is returned.

The unzipped firmware file can then be passed to `extractFirmware` to get the firmware data. Example usage:
The unzipped firmware file can then be passed to `extractFirmwareAsync` to get the firmware data. Example usage:

```ts
// Unzip the firmware archive
Expand All @@ -468,7 +468,7 @@ const { filename, format, rawData } = unzippedFirmware;
// Extract the firmware from a given firmware file
let actualFirmware: Firmware;
try {
actualFirmware = extractFirmware(rawData, format);
actualFirmware = await extractFirmwareAsync(rawData, format);
} catch (e) {
// handle the error, then abort the update
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cc/src/cc/SecurityCC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
getCCName,
isTransmissionError,
parseCCList,
randomBytes,
validatePayload,
} from "@zwave-js/core";
import { type MaybeNotKnown } from "@zwave-js/core/safe";
Expand All @@ -35,7 +36,6 @@ import type {
import { Bytes } from "@zwave-js/shared/safe";
import { buffer2hex, num2hex, pick } from "@zwave-js/shared/safe";
import { wait } from "alcalzone-shared/async";
import { randomBytes } from "node:crypto";
import { CCAPI, PhysicalCCAPI } from "../lib/API.js";
import {
type CCRaw,
Expand Down
3 changes: 1 addition & 2 deletions packages/cc/src/index_browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

export * from "./lib/Security2/shared.js";
export * from "./lib/SetValueResult.js";
export * from "./lib/SetValueResult.js";
// eslint-disable-next-line @zwave-js/no-forbidden-imports -- FIXME: This is actually wrong, but I need to get the release done
export { defaultCCValueOptions } from "./lib/Values.js";
export type {
CCValueOptions,
CCValuePredicate,
PartialCCValuePredicate,
} from "./lib/Values.js";
export * from "./lib/_Types.js";
export * from "./lib/_Types.js";
1 change: 1 addition & 0 deletions packages/cc/src/index_safe.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* @forbiddenImports external */

export * from "./lib/SetValueResult.js";
// eslint-disable-next-line @zwave-js/no-forbidden-imports -- FIXME: This is actually wrong, but I need to get the release done
export * from "./lib/_Types.js";
10 changes: 5 additions & 5 deletions packages/config/src/devices/DeviceConfig.hash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ test(
))!;
t.expect(config).toBeDefined();

const hash = config.getHash();
const hash = await config.getHash();
t.expect(isUint8Array(hash)).toBe(true);
},
// This test might take a while
Expand All @@ -42,11 +42,11 @@ test(
))!;
t.expect(config).toBeDefined();

const hash1 = config.getHash();
const hash1 = await config.getHash();

// @ts-expect-error
config.paramInformation!.get({ parameter: 2 })!.unit = "lightyears";
const hash2 = config.getHash();
const hash2 = await config.getHash();

t.expect(hash1).not.toStrictEqual(hash2);
},
Expand All @@ -68,14 +68,14 @@ test(
))!;
t.expect(config).toBeDefined();

const hash1 = config.getHash();
const hash1 = await config.getHash();

const removeCCs = new Map();
removeCCs.set(CommandClasses["All Switch"], "*");
// @ts-expect-error
config.compat!.removeCCs = removeCCs;

const hash2 = config.getHash();
const hash2 = await config.getHash();

t.expect(hash1).not.toStrictEqual(hash2);
},
Expand Down
23 changes: 15 additions & 8 deletions packages/config/src/devices/DeviceConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core";
import { digest } from "@zwave-js/core";
import {
Bytes,
type JSONObject,
Expand All @@ -12,7 +13,6 @@ import {
} from "@zwave-js/shared";
import { isArray, isObject } from "alcalzone-shared/typeguards";
import JSON5 from "json5";
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import semverGt from "semver/functions/gt.js";
Expand Down Expand Up @@ -761,10 +761,7 @@ export class DeviceConfig {
}
}

/**
* Returns a hash code that can be used to check whether a device config has changed enough to require a re-interview.
*/
public getHash(): Uint8Array {
private getHashable(): Uint8Array {
// We only need to compare the information that is persisted elsewhere:
// - config parameters
// - functional association settings
Expand Down Expand Up @@ -902,9 +899,19 @@ export class DeviceConfig {

hashable = sortObject(hashable);

return Bytes.from(JSON.stringify(hashable), "utf8");
}

/**
* Returns a hash code that can be used to check whether a device config has changed enough to require a re-interview.
*/
public getHash(
algorithm: "md5" | "sha-256" = "sha-256",
): Promise<Uint8Array> {
// Figure out what to hash
const buffer = this.getHashable();

// And create a hash from it. This does not need to be cryptographically secure, just good enough to detect changes.
const buffer = Bytes.from(JSON.stringify(hashable), "utf8");
const md5 = createHash("md5");
return md5.update(buffer).digest();
return digest(algorithm, buffer);
}
}
5 changes: 5 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@
"import": "./build/esm/registries/index.js",
"require": "./build/cjs/registries/index.js"
},
"./traits": {
"@@dev": "./src/traits/index.ts",
"import": "./build/esm/traits/index.js",
"require": "./build/cjs/traits/index.js"
},
"./qr": {
"@@dev": "./src/qr/index.ts",
"browser": "./build/esm/qr/index.browser.js",
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/crypto/index.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ export {
computePRK as computePRKAsync,
decryptAES128CCM as decryptAES128CCMAsync,
decryptAES128OFB as decryptAES128OFBAsync,
decryptAES256CBC as decryptAES256CBCAsync,
deriveMEI as deriveMEIAsync,
deriveNetworkKeys as deriveNetworkKeysAsync,
deriveTempKeys as deriveTempKeysAsync,
digest,
encryptAES128CCM as encryptAES128CCMAsync,
encryptAES128ECB as encryptAES128ECBAsync,
encryptAES128OFB as encryptAES128OFBAsync,
randomBytes as randomBytesAsync,
randomBytes,
} from "./operations.async.js";
7 changes: 5 additions & 2 deletions packages/core/src/crypto/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ export {
computePRK as computePRKAsync,
decryptAES128CCM as decryptAES128CCMAsync,
decryptAES128OFB as decryptAES128OFBAsync,
decryptAES256CBC as decryptAES256CBCAsync,
deriveMEI as deriveMEIAsync,
deriveNetworkKeys as deriveNetworkKeysAsync,
deriveTempKeys as deriveTempKeysAsync,
digest,
encryptAES128CCM as encryptAES128CCMAsync,
encryptAES128ECB as encryptAES128ECBAsync,
encryptAES128OFB as encryptAES128OFBAsync,
randomBytes as randomBytesAsync,
randomBytes,
} from "./operations.async.js";
export {
computeCMAC as computeCMACSync,
Expand All @@ -29,8 +31,9 @@ export {
computePRK as computePRKSync,
decryptAES128CCM as decryptAES128CCMSync,
decryptAES128OFB as decryptAES128OFBSync,
decryptAES256CBC as decryptAES256CBCSync,
encryptAES128CCM as encryptAES128CCMSync,
encryptAES128ECB as encryptAES128ECBSync,
encryptAES128OFB as encryptAES128OFBSync,
randomBytes as randomBytesSync,
// No need to export randomBytes here, the portable version is also synchronous
} from "./operations.sync.js";
6 changes: 5 additions & 1 deletion packages/core/src/crypto/operations.async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@ import { Bytes } from "@zwave-js/shared/safe";
import { BLOCK_SIZE, leftShift1, xor, zeroPad } from "./shared.js";

// Import the correct primitives based on the environment
import primitives from "#crypto_primitives";
import { primitives } from "#crypto_primitives";
const {
decryptAES128OFB,
encryptAES128CBC,
encryptAES128ECB,
encryptAES128OFB,
encryptAES128CCM,
decryptAES128CCM,
decryptAES256CBC,
randomBytes,
digest,
} = primitives;

export {
decryptAES128CCM,
decryptAES128OFB,
decryptAES256CBC,
digest,
encryptAES128CBC,
encryptAES128CCM,
encryptAES128ECB,
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/crypto/operations.sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@ export const decryptAES128OFB = decrypt.bind(
true,
);

/**
* Decrypts a payload using AES-256-CBC
* @deprecated Use the async version of this function instead
*/
export const decryptAES256CBC = decrypt.bind(
undefined,
"aes-256-cbc",
16,
true,
);

/**
* Computes a message authentication code for Security S0 (as described in SDS10865)
* @deprecated Use the async version of this function instead
Expand Down
44 changes: 43 additions & 1 deletion packages/core/src/crypto/primitives/primitives.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,33 @@ async function encryptAES128CBC(
return new Uint8Array(ciphertext, 0, paddedLength);
}

/** Decrypts a payload using AES-256-CBC */
async function decryptAES256CBC(
ciphertext: Uint8Array,
key: Uint8Array,
iv: Uint8Array,
): Promise<Uint8Array> {
const cryptoKey = await subtle.importKey(
"raw",
key,
{ name: "AES-CBC" },
true,
["decrypt"],
);

const plaintext = await subtle.decrypt(
{
name: "AES-CBC",
iv,
},
cryptoKey,
ciphertext,
);

// Padding is removed automatically
return new Uint8Array(plaintext);
}

/** Encrypts a payload using AES-128-OFB */
async function encryptAES128OFB(
plaintext: Uint8Array,
Expand Down Expand Up @@ -359,12 +386,27 @@ async function decryptAES128CCM(
}
}

export default {
function digest(
algorithm: "md5" | "sha-1" | "sha-256",
data: Uint8Array,
): Promise<Uint8Array> {
// The WebCrypto API does not support MD5, but we don't actually care.
// MD5 is only used for hashing cached device configurations, and if anyone
// is going to use these methods, they should be on a new installation anyways.
if (algorithm === "md5") {
algorithm = "sha-256";
}
return subtle.digest(algorithm, data);
}

export const primitives = {
randomBytes,
encryptAES128ECB,
encryptAES128CBC,
encryptAES128OFB,
decryptAES128OFB,
encryptAES128CCM,
decryptAES128CCM,
decryptAES256CBC,
digest,
} satisfies CryptoPrimitives;
31 changes: 30 additions & 1 deletion packages/core/src/crypto/primitives/primitives.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,24 @@ function encryptAES128CBC(
);
}

/** Decrypts a payload using AES-256-CBC */
function decryptAES256CBC(
ciphertext: Uint8Array,
key: Uint8Array,
iv: Uint8Array,
): Promise<Uint8Array> {
return Promise.resolve(
decrypt(
"aes-256-cbc",
BLOCK_SIZE,
true,
ciphertext,
key,
iv,
),
);
}

function encryptAES128CCM(
plaintext: Uint8Array,
key: Uint8Array,
Expand Down Expand Up @@ -171,12 +189,23 @@ function decryptAES128CCM(
return Promise.resolve({ plaintext, authOK });
}

export default {
function digest(
algorithm: "md5" | "sha-1" | "sha-256",
data: Uint8Array,
): Promise<Uint8Array> {
const hash = crypto.createHash(algorithm);
hash.update(data);
return Promise.resolve(hash.digest());
}

export const primitives = {
randomBytes,
encryptAES128ECB,
encryptAES128CBC,
encryptAES128OFB,
decryptAES128OFB,
encryptAES128CCM,
decryptAES128CCM,
decryptAES256CBC,
digest,
} satisfies CryptoPrimitives;
Loading

0 comments on commit 9703274

Please sign in to comment.