@riddix/hamh 2.1.0-alpha.591 → 2.1.0-alpha.593

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10026,7 +10026,7 @@ function MockCrypto(index = 128, implementation = StandardCrypto) {
10026
10026
  throw new ImplementationError(`Index for stable crypto must be 0-255`);
10027
10027
  }
10028
10028
  const crypto8 = new implementation();
10029
- const { randomBytes: randomBytes2, createKeyPair } = crypto8;
10029
+ const { randomBytes: randomBytes3, createKeyPair } = crypto8;
10030
10030
  Object.defineProperties(crypto8, {
10031
10031
  index: {
10032
10032
  get() {
@@ -10038,11 +10038,11 @@ function MockCrypto(index = 128, implementation = StandardCrypto) {
10038
10038
  },
10039
10039
  entropic: {
10040
10040
  get() {
10041
- return crypto8.randomBytes === randomBytes2;
10041
+ return crypto8.randomBytes === randomBytes3;
10042
10042
  },
10043
10043
  set(entropic) {
10044
10044
  if (entropic) {
10045
- crypto8.randomBytes = randomBytes2;
10045
+ crypto8.randomBytes = randomBytes3;
10046
10046
  crypto8.createKeyPair = createKeyPair;
10047
10047
  } else {
10048
10048
  disableEntropy();
@@ -148035,6 +148035,17 @@ async function extractBackupData(buffer) {
148035
148035
  const data = JSON.parse(content.toString());
148036
148036
  return { backupData: data, zipDirectory: directory };
148037
148037
  }
148038
+ function resolveWithin(baseDir, relative) {
148039
+ if (relative.length === 0 || path.isAbsolute(relative)) {
148040
+ return null;
148041
+ }
148042
+ const resolvedBase = path.resolve(baseDir);
148043
+ const resolvedTarget = path.resolve(resolvedBase, relative);
148044
+ if (resolvedTarget !== resolvedBase && !resolvedTarget.startsWith(resolvedBase + path.sep)) {
148045
+ return null;
148046
+ }
148047
+ return resolvedTarget;
148048
+ }
148038
148049
  async function restoreIdentityFiles(zipDirectory, bridgeId, storageLocation) {
148039
148050
  const identityPrefix = `identity/${bridgeId}/`;
148040
148051
  const identityFiles = zipDirectory.files.filter(
@@ -148047,7 +148058,12 @@ async function restoreIdentityFiles(zipDirectory, bridgeId, storageLocation) {
148047
148058
  fs.mkdirSync(targetDir, { recursive: true });
148048
148059
  for (const file of identityFiles) {
148049
148060
  const relativePath = file.path.substring(identityPrefix.length);
148050
- const targetPath = path.join(targetDir, relativePath);
148061
+ const targetPath = resolveWithin(targetDir, relativePath);
148062
+ if (!targetPath) {
148063
+ throw new Error(
148064
+ `Refusing to restore identity file with unsafe path: ${file.path}`
148065
+ );
148066
+ }
148051
148067
  const targetDirPath = path.dirname(targetPath);
148052
148068
  fs.mkdirSync(targetDirPath, { recursive: true });
148053
148069
  const content = await file.buffer();
@@ -148067,7 +148083,12 @@ async function restoreBridgeIcon(zipDirectory, bridgeId, storageLocation) {
148067
148083
  fs.mkdirSync(iconsDir, { recursive: true });
148068
148084
  for (const file of iconFiles) {
148069
148085
  const fileName = file.path.substring(iconPrefix.length);
148070
- const targetPath = path.join(iconsDir, fileName);
148086
+ const targetPath = resolveWithin(iconsDir, fileName);
148087
+ if (!targetPath) {
148088
+ throw new Error(
148089
+ `Refusing to restore bridge icon with unsafe path: ${file.path}`
148090
+ );
148091
+ }
148071
148092
  const content = await file.buffer();
148072
148093
  fs.writeFileSync(targetPath, content);
148073
148094
  }
@@ -151436,16 +151457,25 @@ var WebApi = class extends Service {
151436
151457
  });
151437
151458
  }
151438
151459
  createDynamicAuthMiddleware() {
151460
+ const envAuth = this.props.auth;
151461
+ const envMiddleware = envAuth ? basicAuth({
151462
+ users: { [envAuth.username]: envAuth.password },
151463
+ challenge: true,
151464
+ realm: "Home Assistant Matter Hub"
151465
+ }) : void 0;
151466
+ const storageMiddleware = basicAuth({
151467
+ authorizer: (username, password) => this.settingsStorage.verifyAuth(username, password),
151468
+ challenge: true,
151469
+ realm: "Home Assistant Matter Hub"
151470
+ });
151439
151471
  return (req, res, next) => {
151440
- const auth = this.props.auth ?? this.settingsStorage.auth;
151441
- if (!auth) {
151472
+ if (envMiddleware) {
151473
+ return envMiddleware(req, res, next);
151474
+ }
151475
+ if (!this.settingsStorage.auth) {
151442
151476
  return next();
151443
151477
  }
151444
- return basicAuth({
151445
- users: { [auth.username]: auth.password },
151446
- challenge: true,
151447
- realm: "Home Assistant Matter Hub"
151448
- })(req, res, next);
151478
+ return storageMiddleware(req, res, next);
151449
151479
  };
151450
151480
  }
151451
151481
  async start() {
@@ -151539,39 +151569,43 @@ var BetterLogger = class _BetterLogger extends Logger {
151539
151569
  }
151540
151570
  }
151541
151571
  debug(...values4) {
151542
- const message = values4.map((v) => String(v)).join(" ");
151543
- addLogEntry({
151544
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
151545
- level: "debug",
151546
- message: `[${this.loggerName}] ${message}`
151547
- });
151572
+ if (this._level <= LogLevel.DEBUG) {
151573
+ addLogEntry({
151574
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
151575
+ level: "debug",
151576
+ message: `[${this.loggerName}] ${values4.map((v) => String(v)).join(" ")}`
151577
+ });
151578
+ }
151548
151579
  super.debug(...values4);
151549
151580
  }
151550
151581
  info(...values4) {
151551
- const message = values4.map((v) => String(v)).join(" ");
151552
- addLogEntry({
151553
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
151554
- level: "info",
151555
- message: `[${this.loggerName}] ${message}`
151556
- });
151582
+ if (this._level <= LogLevel.INFO) {
151583
+ addLogEntry({
151584
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
151585
+ level: "info",
151586
+ message: `[${this.loggerName}] ${values4.map((v) => String(v)).join(" ")}`
151587
+ });
151588
+ }
151557
151589
  super.info(...values4);
151558
151590
  }
151559
151591
  warn(...values4) {
151560
- const message = values4.map((v) => String(v)).join(" ");
151561
- addLogEntry({
151562
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
151563
- level: "warn",
151564
- message: `[${this.loggerName}] ${message}`
151565
- });
151592
+ if (this._level <= LogLevel.WARN) {
151593
+ addLogEntry({
151594
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
151595
+ level: "warn",
151596
+ message: `[${this.loggerName}] ${values4.map((v) => String(v)).join(" ")}`
151597
+ });
151598
+ }
151566
151599
  super.warn(...values4);
151567
151600
  }
151568
151601
  error(...values4) {
151569
- const message = values4.map((v) => String(v)).join(" ");
151570
- addLogEntry({
151571
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
151572
- level: "error",
151573
- message: `[${this.loggerName}] ${message}`
151574
- });
151602
+ if (this._level <= LogLevel.ERROR) {
151603
+ addLogEntry({
151604
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
151605
+ level: "error",
151606
+ message: `[${this.loggerName}] ${values4.map((v) => String(v)).join(" ")}`
151607
+ });
151608
+ }
151575
151609
  super.error(...values4);
151576
151610
  }
151577
151611
  debugCtx(message, context) {
@@ -152643,10 +152677,56 @@ function mockDeviceId(entityId) {
152643
152677
 
152644
152678
  // src/services/storage/app-settings-storage.ts
152645
152679
  init_service();
152680
+ import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
152681
+ var SCRYPT_KEYLEN = 64;
152682
+ var SCRYPT_COST_N = 16384;
152683
+ var SCRYPT_BLOCK_R = 8;
152684
+ var SCRYPT_PARALLEL_P = 1;
152685
+ var SALT_BYTES = 16;
152646
152686
  var DEFAULT_BACKUP_SETTINGS = {
152647
152687
  autoBackup: true,
152648
152688
  backupRetentionCount: 5
152649
152689
  };
152690
+ function hashPassword(plain2) {
152691
+ const salt = randomBytes(SALT_BYTES);
152692
+ const derived = scryptSync(plain2, salt, SCRYPT_KEYLEN, {
152693
+ N: SCRYPT_COST_N,
152694
+ r: SCRYPT_BLOCK_R,
152695
+ p: SCRYPT_PARALLEL_P
152696
+ });
152697
+ return {
152698
+ passwordHash: derived.toString("hex"),
152699
+ passwordSalt: salt.toString("hex")
152700
+ };
152701
+ }
152702
+ function verifyPasswordHash(plain2, passwordHash, passwordSalt) {
152703
+ let expected;
152704
+ let salt;
152705
+ try {
152706
+ expected = Buffer.from(passwordHash, "hex");
152707
+ salt = Buffer.from(passwordSalt, "hex");
152708
+ } catch {
152709
+ return false;
152710
+ }
152711
+ if (expected.length !== SCRYPT_KEYLEN || salt.length === 0) {
152712
+ return false;
152713
+ }
152714
+ const derived = scryptSync(plain2, salt, SCRYPT_KEYLEN, {
152715
+ N: SCRYPT_COST_N,
152716
+ r: SCRYPT_BLOCK_R,
152717
+ p: SCRYPT_PARALLEL_P
152718
+ });
152719
+ return timingSafeEqual(derived, expected);
152720
+ }
152721
+ function constantTimeStringEquals(a, b) {
152722
+ const bufA = Buffer.from(a);
152723
+ const bufB = Buffer.from(b);
152724
+ if (bufA.length !== bufB.length) {
152725
+ timingSafeEqual(bufA, bufA);
152726
+ return false;
152727
+ }
152728
+ return timingSafeEqual(bufA, bufB);
152729
+ }
152650
152730
  var AppSettingsStorage = class extends Service {
152651
152731
  constructor(appStorage) {
152652
152732
  super("AppSettingsStorage");
@@ -152654,6 +152734,11 @@ var AppSettingsStorage = class extends Service {
152654
152734
  }
152655
152735
  storage;
152656
152736
  settings = {};
152737
+ // Caches the last plaintext password that was successfully verified
152738
+ // against the stored hash, keyed by username. This lets repeated basic-auth
152739
+ // calls avoid re-running scrypt on every HTTP request without persisting
152740
+ // the plaintext to disk.
152741
+ verifiedPassword = null;
152657
152742
  async initialize() {
152658
152743
  this.storage = this.appStorage.createContext("settings");
152659
152744
  const stored = await this.storage.get(
@@ -152663,12 +152748,72 @@ var AppSettingsStorage = class extends Service {
152663
152748
  this.settings = stored ?? {};
152664
152749
  }
152665
152750
  get auth() {
152666
- return this.settings.auth;
152751
+ if (!this.settings.auth) {
152752
+ return void 0;
152753
+ }
152754
+ return { username: this.settings.auth.username };
152667
152755
  }
152668
- async setAuth(auth) {
152669
- this.settings.auth = auth;
152756
+ async setAuth(credentials) {
152757
+ if (!credentials) {
152758
+ this.settings.auth = void 0;
152759
+ this.verifiedPassword = null;
152760
+ } else {
152761
+ const { passwordHash, passwordSalt } = hashPassword(credentials.password);
152762
+ this.settings.auth = {
152763
+ username: credentials.username,
152764
+ passwordHash,
152765
+ passwordSalt
152766
+ };
152767
+ this.verifiedPassword = {
152768
+ username: credentials.username,
152769
+ password: credentials.password
152770
+ };
152771
+ }
152670
152772
  await this.persist();
152671
152773
  }
152774
+ verifyAuth(username, password) {
152775
+ const stored = this.settings.auth;
152776
+ if (!stored) {
152777
+ return false;
152778
+ }
152779
+ if (!constantTimeStringEquals(username, stored.username)) {
152780
+ return false;
152781
+ }
152782
+ if (this.verifiedPassword && this.verifiedPassword.username === stored.username && constantTimeStringEquals(password, this.verifiedPassword.password)) {
152783
+ return true;
152784
+ }
152785
+ if (stored.password !== void 0) {
152786
+ const matches = constantTimeStringEquals(password, stored.password);
152787
+ if (matches) {
152788
+ this.verifiedPassword = { username: stored.username, password };
152789
+ void this.migrateLegacyPlaintext(password);
152790
+ }
152791
+ return matches;
152792
+ }
152793
+ if (stored.passwordHash && stored.passwordSalt) {
152794
+ if (verifyPasswordHash(password, stored.passwordHash, stored.passwordSalt)) {
152795
+ this.verifiedPassword = { username: stored.username, password };
152796
+ return true;
152797
+ }
152798
+ }
152799
+ return false;
152800
+ }
152801
+ async migrateLegacyPlaintext(plain2) {
152802
+ const current = this.settings.auth;
152803
+ if (!current || current.password === void 0) {
152804
+ return;
152805
+ }
152806
+ const { passwordHash, passwordSalt } = hashPassword(plain2);
152807
+ this.settings.auth = {
152808
+ username: current.username,
152809
+ passwordHash,
152810
+ passwordSalt
152811
+ };
152812
+ try {
152813
+ await this.persist();
152814
+ } catch {
152815
+ }
152816
+ }
152672
152817
  get backupSettings() {
152673
152818
  return { ...DEFAULT_BACKUP_SETTINGS, ...this.settings.backup };
152674
152819
  }
@@ -152977,7 +153122,7 @@ var EntityMappingStorage = class extends Service {
152977
153122
 
152978
153123
  // src/services/storage/lock-credential-storage.ts
152979
153124
  init_service();
152980
- import { pbkdf2Sync, randomBytes } from "node:crypto";
153125
+ import { pbkdf2Sync, randomBytes as randomBytes2 } from "node:crypto";
152981
153126
  var CURRENT_VERSION2 = 2;
152982
153127
  var HASH_ITERATIONS = 1e5;
152983
153128
  var HASH_KEY_LENGTH = 64;
@@ -153014,7 +153159,7 @@ var LockCredentialStorage = class extends Service {
153014
153159
  async migrate(data) {
153015
153160
  if (data.version === 1) {
153016
153161
  for (const legacyCredential of data.credentials) {
153017
- const salt = randomBytes(SALT_LENGTH).toString("hex");
153162
+ const salt = randomBytes2(SALT_LENGTH).toString("hex");
153018
153163
  const hash2 = this.hashPin(legacyCredential.pinCode, salt);
153019
153164
  const credential = {
153020
153165
  entityId: legacyCredential.entityId,
@@ -153080,7 +153225,7 @@ var LockCredentialStorage = class extends Service {
153080
153225
  async setCredential(request) {
153081
153226
  const now = Date.now();
153082
153227
  const existing = this.credentials.get(request.entityId);
153083
- const salt = randomBytes(SALT_LENGTH).toString("hex");
153228
+ const salt = randomBytes2(SALT_LENGTH).toString("hex");
153084
153229
  const hash2 = this.hashPin(request.pinCode, salt);
153085
153230
  const credential = {
153086
153231
  entityId: request.entityId,